From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> Date: Mon, 1 Aug 2022 22:50:34 -0400 Subject: [PATCH] Brigadier based command API == AT == public net.minecraft.commands.arguments.blocks.BlockInput tag public net.minecraft.commands.arguments.DimensionArgument ERROR_INVALID_VALUE public net.minecraft.server.ReloadableServerResources registryLookup public net.minecraft.server.ReloadableServerResources Co-authored-by: Jake Potrebic <jake.m.potrebic@gmail.com> diff --git a/src/main/java/com/mojang/brigadier/CommandDispatcher.java b/src/main/java/com/mojang/brigadier/CommandDispatcher.java index 4b4f812eb13d5f03bcf3f8724d8aa8dbbc724e8b..a4d5d7017e0be79844b996de85a63cad5f8488bc 100644 --- a/src/main/java/com/mojang/brigadier/CommandDispatcher.java +++ b/src/main/java/com/mojang/brigadier/CommandDispatcher.java @@ -459,7 +459,7 @@ public class CommandDispatcher<S> { } private String getSmartUsage(final CommandNode<S> node, final S source, final boolean optional, final boolean deep) { - if (!node.canUse(source)) { + if (source != null && !node.canUse(source)) { // Paper return null; } @@ -473,7 +473,7 @@ public class CommandDispatcher<S> { final String redirect = node.getRedirect() == this.root ? "..." : "-> " + node.getRedirect().getUsageText(); return self + CommandDispatcher.ARGUMENT_SEPARATOR + redirect; } else { - final Collection<CommandNode<S>> children = node.getChildren().stream().filter(c -> c.canUse(source)).collect(Collectors.toList()); + final Collection<CommandNode<S>> children = node.getChildren().stream().filter(c -> source == null || c.canUse(source)).collect(Collectors.toList()); // Paper if (children.size() == 1) { final String usage = this.getSmartUsage(children.iterator().next(), source, childOptional, childOptional); if (usage != null) { diff --git a/src/main/java/com/mojang/brigadier/tree/CommandNode.java b/src/main/java/com/mojang/brigadier/tree/CommandNode.java index 1f4963bf4681a771130abc1da179819626ecfc1f..03ce8a2abb6dceaa922dcce7f3adbc228bbde4bc 100644 --- a/src/main/java/com/mojang/brigadier/tree/CommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/CommandNode.java @@ -35,6 +35,8 @@ public abstract class CommandNode<S> implements Comparable<CommandNode<S>> { private final boolean forks; private Command<S> command; public CommandNode<CommandSourceStack> clientNode; // Paper - Brigadier API + public CommandNode<io.papermc.paper.command.brigadier.CommandSourceStack> unwrappedCached = null; // Paper - Brigadier Command API + public CommandNode<io.papermc.paper.command.brigadier.CommandSourceStack> wrappedCached = null; // Paper - Brigadier Command API // CraftBukkit start public void removeCommand(String name) { this.children.remove(name); @@ -203,4 +205,11 @@ public abstract class CommandNode<S> implements Comparable<CommandNode<S>> { } public abstract Collection<String> getExamples(); + // Paper start - Brigadier Command API + public void clearAll() { + this.children.clear(); + this.literals.clear(); + this.arguments.clear(); + } + // Paper end - Brigadier Command API } diff --git a/src/main/java/io/papermc/paper/brigadier/NullCommandSender.java b/src/main/java/io/papermc/paper/brigadier/NullCommandSender.java new file mode 100644 index 0000000000000000000000000000000000000000..367ef7e0769537e8c13c7fd818a1249e15a28a65 --- /dev/null +++ b/src/main/java/io/papermc/paper/brigadier/NullCommandSender.java @@ -0,0 +1,151 @@ +package io.papermc.paper.brigadier; + +import java.util.Set; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.md_5.bungee.api.chat.BaseComponent; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.command.CommandSender; +import org.bukkit.permissions.PermissibleBase; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionAttachment; +import org.bukkit.permissions.PermissionAttachmentInfo; +import org.bukkit.plugin.Plugin; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@DefaultQualifier(NonNull.class) +public final class NullCommandSender implements CommandSender { + + public static final CommandSender INSTANCE = new NullCommandSender(); + + private NullCommandSender() { + } + + @Override + public void sendMessage(final String message) { + } + + @Override + public void sendMessage(final String... messages) { + } + + @Override + public void sendMessage(@Nullable final UUID sender, final String message) { + } + + @Override + public void sendMessage(@Nullable final UUID sender, final String... messages) { + } + + @SuppressWarnings("ConstantValue") + @Override + public Server getServer() { + final @Nullable Server server = Bukkit.getServer(); + if (server == null) { + throw new UnsupportedOperationException("The server has not been created yet, you cannot access it at this time from the 'null' CommandSender"); + } + return server; + } + + @Override + public String getName() { + return ""; + } + + private final Spigot spigot = new Spigot(); + @Override + public Spigot spigot() { + return this.spigot; + } + + public final class Spigot extends CommandSender.Spigot { + + @Override + public void sendMessage(@NotNull final BaseComponent component) { + } + + @Override + public void sendMessage(@NonNull final @NotNull BaseComponent... components) { + } + + @Override + public void sendMessage(@Nullable final UUID sender, @NotNull final BaseComponent component) { + } + + @Override + public void sendMessage(@Nullable final UUID sender, @NonNull final @NotNull BaseComponent... components) { + } + } + + @Override + public Component name() { + return Component.empty(); + } + + @Override + public boolean isPermissionSet(final String name) { + return false; + } + + @Override + public boolean isPermissionSet(final Permission perm) { + return false; + } + + @Override + public boolean hasPermission(final String name) { + return true; + } + + @Override + public boolean hasPermission(final Permission perm) { + return true; + } + + @Override + public PermissionAttachment addAttachment(final Plugin plugin, final String name, final boolean value) { + throw new UnsupportedOperationException("Cannot add attachments to the 'null' CommandSender"); + } + + @Override + public PermissionAttachment addAttachment(final Plugin plugin) { + throw new UnsupportedOperationException("Cannot add attachments to the 'null' CommandSender"); + } + + @Override + public @Nullable PermissionAttachment addAttachment(final Plugin plugin, final String name, final boolean value, final int ticks) { + throw new UnsupportedOperationException("Cannot add attachments to the 'null' CommandSender"); + } + + @Override + public @Nullable PermissionAttachment addAttachment(final Plugin plugin, final int ticks) { + throw new UnsupportedOperationException("Cannot add attachments to the 'null' CommandSender"); + } + + @Override + public void removeAttachment(final PermissionAttachment attachment) { + throw new UnsupportedOperationException("Cannot add attachments to the 'null' CommandSender"); + } + + @Override + public void recalculatePermissions() { + } + + @Override + public Set<PermissionAttachmentInfo> getEffectivePermissions() { + throw new UnsupportedOperationException("Cannot remove attachments from the 'null' CommandSender"); + } + + @Override + public boolean isOp() { + return true; + } + + @Override + public void setOp(final boolean value) { + } +} diff --git a/src/main/java/io/papermc/paper/brigadier/PaperBrigadierProviderImpl.java b/src/main/java/io/papermc/paper/brigadier/PaperBrigadierProviderImpl.java deleted file mode 100644 index dd6012b6a097575b2d1471be5069eccee4537c0a..0000000000000000000000000000000000000000 --- a/src/main/java/io/papermc/paper/brigadier/PaperBrigadierProviderImpl.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.papermc.paper.brigadier; - -import com.mojang.brigadier.Message; -import io.papermc.paper.adventure.PaperAdventure; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.ComponentLike; -import net.minecraft.network.chat.ComponentUtils; -import org.checkerframework.checker.nullness.qual.NonNull; - -import static java.util.Objects.requireNonNull; - -public enum PaperBrigadierProviderImpl implements PaperBrigadierProvider { - INSTANCE; - - PaperBrigadierProviderImpl() { - PaperBrigadierProvider.initialize(this); - } - - @Override - public @NonNull Message message(final @NonNull ComponentLike componentLike) { - requireNonNull(componentLike, "componentLike"); - return PaperAdventure.asVanilla(componentLike.asComponent()); - } - - @Override - public @NonNull Component componentFromMessage(final @NonNull Message message) { - requireNonNull(message, "message"); - return PaperAdventure.asAdventure(ComponentUtils.fromMessage(message)); - } -} diff --git a/src/main/java/io/papermc/paper/command/brigadier/ApiMirrorRootNode.java b/src/main/java/io/papermc/paper/command/brigadier/ApiMirrorRootNode.java new file mode 100644 index 0000000000000000000000000000000000000000..23525592d880f340745a28c956fa38d3e4057231 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/ApiMirrorRootNode.java @@ -0,0 +1,253 @@ +package io.papermc.paper.command.brigadier; + +import com.google.common.collect.Collections2; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.FloatArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.LongArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.mojang.brigadier.tree.RootCommandNode; +import io.papermc.paper.command.brigadier.argument.CustomArgumentType; +import io.papermc.paper.command.brigadier.argument.VanillaArgumentProviderImpl; +import io.papermc.paper.command.brigadier.argument.WrappedArgumentCommandNode; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Set; +import net.minecraft.commands.synchronization.ArgumentTypeInfos; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * This root command node is responsible for wrapping around vanilla's dispatcher. + * <p> + * The reason for this is conversion is we do NOT want there to be NMS types + * in the api. This allows us to reconstruct the nodes to be more api friendly, while + * we can then unwrap it when needed and convert them to NMS types. + * <p> + * Command nodes such as vanilla (those without a proper "api node") + * will be assigned a {@link ShadowBrigNode}. + * This prevents certain parts of it (children) from being accessed by the api. + */ +public abstract class ApiMirrorRootNode extends RootCommandNode<CommandSourceStack> { + + /** + * Represents argument types that are allowed to exist in the api. + * These typically represent primitives that don't need to be wrapped + * by NMS. + */ + private static final Set<Class<? extends ArgumentType<?>>> ARGUMENT_WHITELIST = Set.of( + BoolArgumentType.class, + DoubleArgumentType.class, + FloatArgumentType.class, + IntegerArgumentType.class, + LongArgumentType.class, + StringArgumentType.class + ); + + public static void validatePrimitiveType(ArgumentType<?> type) { + if (ARGUMENT_WHITELIST.contains(type.getClass())) { + if (!ArgumentTypeInfos.isClassRecognized(type.getClass())) { + throw new IllegalArgumentException("This whitelisted primitive argument type is not recognized by the server!"); + } + } else if (!(type instanceof VanillaArgumentProviderImpl.NativeWrapperArgumentType<?,?> nativeWrapperArgumentType) || !ArgumentTypeInfos.isClassRecognized(nativeWrapperArgumentType.nativeNmsArgumentType().getClass())) { + throw new IllegalArgumentException("Custom argument type was passed, this was not a recognized type to send to the client! You must only pass vanilla arguments or primitive brig args in the wrapper!"); + } + } + + public abstract CommandDispatcher<net.minecraft.commands.CommandSourceStack> getDispatcher(); + + /** + * This logic is responsible for unwrapping an API node to be supported by NMS. + * See the method implementation for detailed steps. + * + * @param maybeWrappedNode api provided node / node to be "wrapped" + * @return wrapped node + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + private @NotNull CommandNode<CommandSourceStack> unwrapNode(final CommandNode<CommandSourceStack> maybeWrappedNode) { + /* + If the type is a shadow node we can assume that the type that it represents is an already supported NMS node. + This is because these are typically minecraft command nodes. + */ + if (maybeWrappedNode instanceof final ShadowBrigNode shadowBrigNode) { + return (CommandNode) shadowBrigNode.getHandle(); + } + + /* + This node already has had an unwrapped node created, so we can assume that it's safe to reuse that cached copy. + */ + if (maybeWrappedNode.unwrappedCached != null) { + return maybeWrappedNode.unwrappedCached; + } + + // convert the pure brig node into one compatible with the nms dispatcher + return this.convertFromPureBrigNode(maybeWrappedNode); + } + + private @NotNull CommandNode<CommandSourceStack> convertFromPureBrigNode(final CommandNode<CommandSourceStack> pureNode) { + /* + Logic for converting a node. + */ + final CommandNode<CommandSourceStack> converted; + if (pureNode instanceof final LiteralCommandNode<CommandSourceStack> node) { + /* + Remap the literal node, we only have to account + for the redirect in this case. + */ + converted = this.simpleUnwrap(node); + } else if (pureNode instanceof final ArgumentCommandNode<CommandSourceStack, ?> pureArgumentNode) { + final ArgumentType<?> pureArgumentType = pureArgumentNode.getType(); + /* + Check to see if this argument type is a wrapped type, if so we know that + we can unwrap the node to get an NMS type. + */ + if (pureArgumentType instanceof final CustomArgumentType<?, ?> customArgumentType) { + final SuggestionProvider<?> suggestionProvider; + try { + final Method listSuggestions = customArgumentType.getClass().getMethod("listSuggestions", CommandContext.class, SuggestionsBuilder.class); + if (listSuggestions.getDeclaringClass() != CustomArgumentType.class) { + suggestionProvider = customArgumentType::listSuggestions; + } else { + suggestionProvider = null; + } + } catch (final NoSuchMethodException ex) { + throw new IllegalStateException("Could not determine if the custom argument type " + customArgumentType + " overrides listSuggestions", ex); + } + + converted = this.unwrapArgumentWrapper(pureArgumentNode, customArgumentType, customArgumentType.getNativeType(), suggestionProvider); + } else if (pureArgumentType instanceof final VanillaArgumentProviderImpl.NativeWrapperArgumentType<?,?> nativeWrapperArgumentType) { + converted = this.unwrapArgumentWrapper(pureArgumentNode, nativeWrapperArgumentType, nativeWrapperArgumentType, null); // "null" for suggestion provider so it uses the argument type's suggestion provider + + /* + If it's not a wrapped type, it either has to be a primitive or an already + defined NMS type. + This method allows us to check if this is recognized by vanilla. + */ + } else if (ArgumentTypeInfos.isClassRecognized(pureArgumentType.getClass())) { + // Allow any type of argument, as long as it's recognized by the client (but in most cases, this should be API only types) + // Previously we only allowed whitelisted types. + converted = this.simpleUnwrap(pureArgumentNode); + } else { + // Unknown argument type was passed + throw new IllegalArgumentException("Custom unknown argument type was passed, should be wrapped inside an CustomArgumentType."); + } + } else { + throw new IllegalArgumentException("Unknown command node passed. Don't know how to unwrap this."); + } + + // Store unwrapped node before unwrapping children to avoid infinite recursion in cyclic redirects. + converted.wrappedCached = pureNode; + pureNode.unwrappedCached = converted; + + /* + Add the children to the node, unwrapping each child in the process. + */ + for (final CommandNode<CommandSourceStack> child : pureNode.getChildren()) { + converted.addChild(this.unwrapNode(child)); + } + + return converted; + } + + /** + * This logic is responsible for rewrapping a node. + * If a node was unwrapped in the past, it should have a wrapped type + * stored in its cache. + * <p> + * However, if it doesn't seem to have a wrapped version we will return + * a {@link ShadowBrigNode} instead. This supports being unwrapped/wrapped while + * preventing the API from accessing it unsafely. + * + * @param unwrapped argument node + * @return wrapped node + */ + private @Nullable CommandNode<CommandSourceStack> wrapNode(@Nullable final CommandNode<net.minecraft.commands.CommandSourceStack> unwrapped) { + if (unwrapped == null) { + return null; + } + + /* + This was most likely created by API and has a wrapped variant, + so we can return this safely. + */ + if (unwrapped.wrappedCached != null) { + return unwrapped.wrappedCached; + } + + /* + We don't know the type of this, or where this came from. + Return a shadow, where we will allow the api to handle this but have + restrictive access. + */ + CommandNode<CommandSourceStack> shadow = new ShadowBrigNode(unwrapped); + unwrapped.wrappedCached = shadow; + return shadow; + } + + /** + * Nodes added to this dispatcher must be unwrapped + * in order to be added to the NMS dispatcher. + * + * @param node node to add + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public void addChild(CommandNode<CommandSourceStack> node) { + CommandNode convertedNode = this.unwrapNode(node); + this.getDispatcher().getRoot().addChild(convertedNode); + } + + /** + * Gets the children for the vanilla dispatcher, + * ensuring that all are wrapped. + * + * @return wrapped children + */ + @Override + public Collection<CommandNode<CommandSourceStack>> getChildren() { + return Collections2.transform(this.getDispatcher().getRoot().getChildren(), this::wrapNode); + } + + @Override + public CommandNode<CommandSourceStack> getChild(String name) { + return this.wrapNode(this.getDispatcher().getRoot().getChild(name)); + } + + // These are needed for bukkit... we should NOT allow this + @Override + public void removeCommand(String name) { + this.getDispatcher().getRoot().removeCommand(name); + } + + @Override + public void clearAll() { + this.getDispatcher().getRoot().clearAll(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private CommandNode<CommandSourceStack> unwrapArgumentWrapper(final ArgumentCommandNode pureNode, final ArgumentType pureArgumentType, final ArgumentType possiblyWrappedNativeArgumentType, @Nullable SuggestionProvider argumentTypeSuggestionProvider) { + validatePrimitiveType(possiblyWrappedNativeArgumentType); + final CommandNode redirectNode = pureNode.getRedirect() == null ? null : this.unwrapNode(pureNode.getRedirect()); + // If there is already a custom suggestion provider, ignore the suggestion provider from the argument type + final SuggestionProvider suggestionProvider = pureNode.getCustomSuggestions() != null ? pureNode.getCustomSuggestions() : argumentTypeSuggestionProvider; + + final ArgumentType nativeArgumentType = possiblyWrappedNativeArgumentType instanceof final VanillaArgumentProviderImpl.NativeWrapperArgumentType<?,?> nativeWrapperArgumentType ? nativeWrapperArgumentType.nativeNmsArgumentType() : possiblyWrappedNativeArgumentType; + return new WrappedArgumentCommandNode<>(pureNode.getName(), pureArgumentType, nativeArgumentType, pureNode.getCommand(), pureNode.getRequirement(), redirectNode, pureNode.getRedirectModifier(), pureNode.isFork(), suggestionProvider); + } + + private CommandNode<CommandSourceStack> simpleUnwrap(final CommandNode<CommandSourceStack> node) { + return node.createBuilder() + .redirect(node.getRedirect() == null ? null : this.unwrapNode(node.getRedirect())) + .build(); + } + +} diff --git a/src/main/java/io/papermc/paper/command/brigadier/MessageComponentSerializerImpl.java b/src/main/java/io/papermc/paper/command/brigadier/MessageComponentSerializerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..0b33c6cf2366568641e6f2fd7f74fb74f6ea0145 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/MessageComponentSerializerImpl.java @@ -0,0 +1,20 @@ +package io.papermc.paper.command.brigadier; + +import com.mojang.brigadier.Message; +import io.papermc.paper.adventure.PaperAdventure; +import net.kyori.adventure.text.Component; +import net.minecraft.network.chat.ComponentUtils; +import org.jetbrains.annotations.NotNull; + +public final class MessageComponentSerializerImpl implements MessageComponentSerializer { + + @Override + public @NotNull Component deserialize(@NotNull Message input) { + return PaperAdventure.asAdventure(ComponentUtils.fromMessage(input)); + } + + @Override + public @NotNull Message serialize(@NotNull Component component) { + return PaperAdventure.asVanilla(component); + } +} diff --git a/src/main/java/io/papermc/paper/command/brigadier/PaperBrigadier.java b/src/main/java/io/papermc/paper/command/brigadier/PaperBrigadier.java new file mode 100644 index 0000000000000000000000000000000000000000..4acf7c3bcfbe61431bfbfa3c8addb33f671eb498 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/PaperBrigadier.java @@ -0,0 +1,73 @@ +package io.papermc.paper.command.brigadier; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import io.papermc.paper.command.brigadier.bukkit.BukkitBrigForwardingMap; +import io.papermc.paper.command.brigadier.bukkit.BukkitCommandNode; +import net.minecraft.commands.CommandSource; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.phys.Vec2; +import net.minecraft.world.phys.Vec3; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.command.Command; +import org.bukkit.command.CommandMap; +import org.bukkit.craftbukkit.command.VanillaCommandWrapper; + +import java.util.Map; + +public final class PaperBrigadier { + + @SuppressWarnings("DataFlowIssue") + static final net.minecraft.commands.CommandSourceStack DUMMY = new net.minecraft.commands.CommandSourceStack( + CommandSource.NULL, + Vec3.ZERO, + Vec2.ZERO, + null, + 4, + "", + CommonComponents.EMPTY, + null, + null + ); + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static Command wrapNode(CommandNode node) { + if (!(node instanceof LiteralCommandNode)) { + throw new IllegalArgumentException("Unsure how to wrap a " + node); + } + + if (!(node instanceof PluginCommandNode pluginCommandNode)) { + return new VanillaCommandWrapper(null, node); + } + CommandNode<CommandSourceStack> argumentCommandNode = node; + if (argumentCommandNode.getRedirect() != null) { + argumentCommandNode = argumentCommandNode.getRedirect(); + } + + Map<CommandNode<CommandSourceStack>, String> map = PaperCommands.INSTANCE.getDispatcherInternal().getSmartUsage(argumentCommandNode, DUMMY); + String usage = map.isEmpty() ? pluginCommandNode.getUsageText() : pluginCommandNode.getUsageText() + " " + String.join("\n" + pluginCommandNode.getUsageText() + " ", map.values()); + return new PluginVanillaCommandWrapper(pluginCommandNode.getName(), pluginCommandNode.getDescription(), usage, pluginCommandNode.getAliases(), node, pluginCommandNode.getPlugin()); + } + + /* + Previously, Bukkit used one command dispatcher and ignored minecraft's reloading logic. + + In order to allow for legacy commands to be properly added, we will iterate through previous bukkit commands + in the old dispatcher and re-register them. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public static void moveBukkitCommands(Commands before, Commands after) { + CommandDispatcher erasedDispatcher = before.getDispatcher(); + + for (Object node : erasedDispatcher.getRoot().getChildren()) { + if (node instanceof CommandNode<?> commandNode && commandNode.getCommand() instanceof BukkitCommandNode.BukkitBrigCommand) { + after.getDispatcher().getRoot().removeCommand(((CommandNode<?>) node).getName()); // Remove already existing commands + after.getDispatcher().getRoot().addChild((CommandNode<net.minecraft.commands.CommandSourceStack>) node); + } + } + } +} diff --git a/src/main/java/io/papermc/paper/command/brigadier/PaperCommandSourceStack.java b/src/main/java/io/papermc/paper/command/brigadier/PaperCommandSourceStack.java new file mode 100644 index 0000000000000000000000000000000000000000..1b1642f306771f029e6214a2e2ebebb6ae6abc3e --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/PaperCommandSourceStack.java @@ -0,0 +1,63 @@ +package io.papermc.paper.command.brigadier; + +import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec2; +import net.minecraft.world.phys.Vec3; +import org.bukkit.Location; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface PaperCommandSourceStack extends CommandSourceStack, BukkitBrigadierCommandSource { + + net.minecraft.commands.CommandSourceStack getHandle(); + + @Override + default @NotNull Location getLocation() { + Vec2 rot = this.getHandle().getRotation(); + Vec3 pos = this.getHandle().getPosition(); + Level level = this.getHandle().getLevel(); + + return new Location(level.getWorld(), pos.x, pos.y, pos.z, rot.y, rot.x); + } + + @Override + @NotNull + default CommandSender getSender() { + return this.getHandle().getBukkitSender(); + } + + @Override + @Nullable + default Entity getExecutor() { + net.minecraft.world.entity.Entity nmsEntity = this.getHandle().getEntity(); + if (nmsEntity == null) { + return null; + } + + return nmsEntity.getBukkitEntity(); + } + + // OLD METHODS + @Override + default org.bukkit.entity.Entity getBukkitEntity() { + return this.getExecutor(); + } + + @Override + default org.bukkit.World getBukkitWorld() { + return this.getLocation().getWorld(); + } + + @Override + default org.bukkit.Location getBukkitLocation() { + return this.getLocation(); + } + + @Override + default CommandSender getBukkitSender() { + return this.getSender(); + } +} diff --git a/src/main/java/io/papermc/paper/command/brigadier/PaperCommands.java b/src/main/java/io/papermc/paper/command/brigadier/PaperCommands.java new file mode 100644 index 0000000000000000000000000000000000000000..95d3b42cbe2184b0a04d941f27f7a6e643ef59be --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/PaperCommands.java @@ -0,0 +1,204 @@ +package io.papermc.paper.command.brigadier; + +import com.google.common.base.Preconditions; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import io.papermc.paper.command.brigadier.bukkit.BukkitCommandNode; +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.lifecycle.event.LifecycleEventOwner; +import io.papermc.paper.plugin.lifecycle.event.registrar.PaperRegistrar; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import net.minecraft.commands.CommandBuildContext; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import static java.util.Objects.requireNonNull; + +@DefaultQualifier(NonNull.class) +public class PaperCommands implements Commands, PaperRegistrar<LifecycleEventOwner> { + + public static final PaperCommands INSTANCE = new PaperCommands(); + + private @Nullable LifecycleEventOwner currentContext; + private @MonotonicNonNull CommandDispatcher<CommandSourceStack> dispatcher; + private @MonotonicNonNull CommandBuildContext buildContext; + private boolean invalid = false; + + @Override + public void setCurrentContext(final @Nullable LifecycleEventOwner context) { + this.currentContext = context; + } + + public void setDispatcher(final net.minecraft.commands.Commands commands, final CommandBuildContext commandBuildContext) { + this.invalid = false; + this.dispatcher = new CommandDispatcher<>(new ApiMirrorRootNode() { + @Override + public CommandDispatcher<net.minecraft.commands.CommandSourceStack> getDispatcher() { + return commands.getDispatcher(); + } + }); + this.buildContext = commandBuildContext; + } + + public void setValid() { + this.invalid = false; + } + + @Override + public void invalidate() { + this.invalid = true; + } + + // use this method internally as it bypasses the valid check + public CommandDispatcher<CommandSourceStack> getDispatcherInternal() { + Preconditions.checkState(this.dispatcher != null, "the dispatcher hasn't been set yet"); + return this.dispatcher; + } + + public CommandBuildContext getBuildContext() { + Preconditions.checkState(this.buildContext != null, "the build context hasn't been set yet"); + return this.buildContext; + } + + @Override + public CommandDispatcher<CommandSourceStack> getDispatcher() { + Preconditions.checkState(!this.invalid && this.dispatcher != null, "cannot access the dispatcher in this context"); + return this.dispatcher; + } + + @Override + public @Unmodifiable Set<String> register(final LiteralCommandNode<CommandSourceStack> node, final @Nullable String description, final Collection<String> aliases) { + return this.register(requireNonNull(this.currentContext, "No lifecycle owner context is set").getPluginMeta(), node, description, aliases); + } + + @Override + public @Unmodifiable Set<String> register(final PluginMeta pluginMeta, final LiteralCommandNode<CommandSourceStack> node, final @Nullable String description, final Collection<String> aliases) { + return this.registerWithFlags(pluginMeta, node, description, aliases, Set.of()); + } + + @Override + public @Unmodifiable Set<String> registerWithFlags(@NotNull final PluginMeta pluginMeta, @NotNull final LiteralCommandNode<CommandSourceStack> node, @org.jetbrains.annotations.Nullable final String description, @NotNull final Collection<String> aliases, @NotNull final Set<CommandRegistrationFlag> flags) { + final boolean hasFlattenRedirectFlag = flags.contains(CommandRegistrationFlag.FLATTEN_ALIASES); + final String identifier = pluginMeta.getName().toLowerCase(Locale.ROOT); + final String literal = node.getLiteral(); + final PluginCommandNode pluginLiteral = new PluginCommandNode(identifier + ":" + literal, pluginMeta, node, description); // Treat the keyed version of the command as the root + + final Set<String> registeredLabels = new HashSet<>(aliases.size() * 2 + 2); + + if (this.registerIntoDispatcher(pluginLiteral, true)) { + registeredLabels.add(pluginLiteral.getLiteral()); + } + if (this.registerRedirect(literal, pluginMeta, pluginLiteral, description, true, hasFlattenRedirectFlag)) { // Plugin commands should override vanilla commands + registeredLabels.add(literal); + } + + // Add aliases + final List<String> registeredAliases = new ArrayList<>(aliases.size() * 2); + for (final String alias : aliases) { + if (this.registerRedirect(alias, pluginMeta, pluginLiteral, description, false, hasFlattenRedirectFlag)) { + registeredAliases.add(alias); + } + if (this.registerRedirect(identifier + ":" + alias, pluginMeta, pluginLiteral, description, false, hasFlattenRedirectFlag)) { + registeredAliases.add(identifier + ":" + alias); + } + } + + if (!registeredAliases.isEmpty()) { + pluginLiteral.setAliases(registeredAliases); + } + + registeredLabels.addAll(registeredAliases); + return registeredLabels.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(registeredLabels); + } + + private boolean registerRedirect(final String aliasLiteral, final PluginMeta plugin, final PluginCommandNode redirectTo, final @Nullable String description, final boolean override, boolean hasFlattenRedirectFlag) { + final LiteralCommandNode<CommandSourceStack> redirect; + if (redirectTo.getChildren().isEmpty() || hasFlattenRedirectFlag) { + redirect = Commands.literal(aliasLiteral) + .executes(redirectTo.getCommand()) + .requires(redirectTo.getRequirement()) + .build(); + + for (final CommandNode<CommandSourceStack> child : redirectTo.getChildren()) { + redirect.addChild(child); + } + } else { + redirect = Commands.literal(aliasLiteral) + .executes(redirectTo.getCommand()) + .redirect(redirectTo) + .requires(redirectTo.getRequirement()) + .build(); + } + + return this.registerIntoDispatcher(new PluginCommandNode(aliasLiteral, plugin, redirect, description), override); + } + + private boolean registerIntoDispatcher(final PluginCommandNode node, boolean override) { + final @Nullable CommandNode<CommandSourceStack> existingChild = this.getDispatcher().getRoot().getChild(node.getLiteral()); + if (existingChild != null && !(existingChild instanceof PluginCommandNode) && !(existingChild instanceof BukkitCommandNode)) { + override = true; // override vanilla commands + } + if (existingChild == null || override) { // Avoid merging behavior. Maybe something to look into in the future + if (override) { + this.getDispatcher().getRoot().removeCommand(node.getLiteral()); + } + this.getDispatcher().getRoot().addChild(node); + return true; + } + + return false; + } + + @Override + public @Unmodifiable Set<String> register(final String label, final @Nullable String description, final Collection<String> aliases, final BasicCommand basicCommand) { + return this.register(requireNonNull(this.currentContext, "No lifecycle owner context is set").getPluginMeta(), label, description, aliases, basicCommand); + } + + @Override + public @Unmodifiable Set<String> register(final PluginMeta pluginMeta, final String label, final @Nullable String description, final Collection<String> aliases, final BasicCommand basicCommand) { + final LiteralArgumentBuilder<CommandSourceStack> builder = Commands.literal(label) + .requires(stack -> basicCommand.canUse(stack.getSender())) + .then( + Commands.argument("args", StringArgumentType.greedyString()) + .suggests((context, suggestionsBuilder) -> { + String[] args = StringUtils.split(suggestionsBuilder.getRemaining()); + if (suggestionsBuilder.getRemaining().endsWith(" ")) { + // if there is trailing whitespace, we should add an empty argument to signify + // that there may be more, but no characters have been typed yet + args = ArrayUtils.add(args, ""); + } + final SuggestionsBuilder offsetSuggestionsBuilder = suggestionsBuilder.createOffset(suggestionsBuilder.getInput().lastIndexOf(' ') + 1); + + final Collection<String> suggestions = basicCommand.suggest(context.getSource(), args); + suggestions.forEach(offsetSuggestionsBuilder::suggest); + return offsetSuggestionsBuilder.buildFuture(); + }) + .executes((stack) -> { + basicCommand.execute(stack.getSource(), StringUtils.split(stack.getArgument("args", String.class), ' ')); + return com.mojang.brigadier.Command.SINGLE_SUCCESS; + }) + ) + .executes((stack) -> { + basicCommand.execute(stack.getSource(), new String[0]); + return com.mojang.brigadier.Command.SINGLE_SUCCESS; + }); + + return this.register(pluginMeta, builder.build(), description, aliases); + } +} diff --git a/src/main/java/io/papermc/paper/command/brigadier/PluginCommandNode.java b/src/main/java/io/papermc/paper/command/brigadier/PluginCommandNode.java new file mode 100644 index 0000000000000000000000000000000000000000..3a9f58873b83f10ba354ae4968c4ab0632662439 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/PluginCommandNode.java @@ -0,0 +1,50 @@ +package io.papermc.paper.command.brigadier; + +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import io.papermc.paper.plugin.configuration.PluginMeta; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class PluginCommandNode extends LiteralCommandNode<CommandSourceStack> { + + private final PluginMeta plugin; + private final String description; + private List<String> aliases = Collections.emptyList(); + + public PluginCommandNode(final @NotNull String literal, final @NotNull PluginMeta plugin, final @NotNull LiteralCommandNode<CommandSourceStack> rootLiteral, final @Nullable String description) { + super( + literal, rootLiteral.getCommand(), rootLiteral.getRequirement(), + rootLiteral.getRedirect(), rootLiteral.getRedirectModifier(), rootLiteral.isFork() + ); + this.plugin = plugin; + this.description = description; + + for (CommandNode<CommandSourceStack> argument : rootLiteral.getChildren()) { + this.addChild(argument); + } + } + + @NotNull + public Plugin getPlugin() { + return Objects.requireNonNull(Bukkit.getPluginManager().getPlugin(this.plugin.getName())); + } + + @NotNull + public String getDescription() { + return this.description; + } + + public void setAliases(List<String> aliases) { + this.aliases = aliases; + } + + public List<String> getAliases() { + return this.aliases; + } +} diff --git a/src/main/java/io/papermc/paper/command/brigadier/PluginVanillaCommandWrapper.java b/src/main/java/io/papermc/paper/command/brigadier/PluginVanillaCommandWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..cf8359af60601d7917e77fd06a00b64992a85953 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/PluginVanillaCommandWrapper.java @@ -0,0 +1,46 @@ +package io.papermc.paper.command.brigadier; + +import com.mojang.brigadier.tree.CommandNode; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import org.bukkit.command.Command; +import org.bukkit.command.PluginIdentifiableCommand; +import org.bukkit.craftbukkit.command.VanillaCommandWrapper; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +// Exists to that /help can show the plugin +public class PluginVanillaCommandWrapper extends VanillaCommandWrapper implements PluginIdentifiableCommand { + + private final Plugin plugin; + private final List<String> alises; + + public PluginVanillaCommandWrapper(String name, String description, String usageMessage, List<String> aliases, CommandNode<CommandSourceStack> vanillaCommand, Plugin plugin) { + super(name, description, usageMessage, aliases, vanillaCommand); + this.plugin = plugin; + this.alises = aliases; + } + + @Override + public @NotNull List<String> getAliases() { + return this.alises; + } + + @Override + public @NotNull Command setAliases(@NotNull List<String> aliases) { + return this; + } + + @Override + public @NotNull Plugin getPlugin() { + return this.plugin; + } + + // Show in help menu! + @Override + public boolean isRegistered() { + return true; + } +} diff --git a/src/main/java/io/papermc/paper/command/brigadier/ShadowBrigNode.java b/src/main/java/io/papermc/paper/command/brigadier/ShadowBrigNode.java new file mode 100644 index 0000000000000000000000000000000000000000..895addef908e09d527e4bc9210599e8827c53807 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/ShadowBrigNode.java @@ -0,0 +1,35 @@ +package io.papermc.paper.command.brigadier; + +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; + +import java.util.Collection; + +public class ShadowBrigNode extends LiteralCommandNode<CommandSourceStack> { + + private final CommandNode<net.minecraft.commands.CommandSourceStack> handle; + + public ShadowBrigNode(CommandNode<net.minecraft.commands.CommandSourceStack> node) { + super(node.getName(), context -> 0, (s) -> false, node.getRedirect() == null ? null : new ShadowBrigNode(node.getRedirect()), null, node.isFork()); + this.handle = node; + } + + @Override + public Collection<CommandNode<CommandSourceStack>> getChildren() { + throw new UnsupportedOperationException("Cannot retrieve children from this node."); + } + + @Override + public CommandNode<CommandSourceStack> getChild(String name) { + throw new UnsupportedOperationException("Cannot retrieve children from this node."); + } + + @Override + public void addChild(CommandNode<CommandSourceStack> node) { + throw new UnsupportedOperationException("Cannot modify children for this node."); + } + + public CommandNode<net.minecraft.commands.CommandSourceStack> getHandle() { + return this.handle; + } +} diff --git a/src/main/java/io/papermc/paper/command/brigadier/argument/SignedMessageResolverImpl.java b/src/main/java/io/papermc/paper/command/brigadier/argument/SignedMessageResolverImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..07a23be2cd7b4592108dee0ae223e71b1d00cec9 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/argument/SignedMessageResolverImpl.java @@ -0,0 +1,30 @@ +package io.papermc.paper.command.brigadier.argument; + +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import java.util.concurrent.CompletableFuture; +import net.kyori.adventure.chat.SignedMessage; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.MessageArgument; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public record SignedMessageResolverImpl(MessageArgument.Message message) implements SignedMessageResolver { + + @Override + public String content() { + return this.message.text(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public CompletableFuture<SignedMessage> resolveSignedMessage(final String argumentName, final CommandContext erased) throws CommandSyntaxException { + final CompletableFuture<SignedMessage> future = new CompletableFuture<>(); + + final MessageArgument.Message response = ((CommandContext<CommandSourceStack>) erased).getArgument(argumentName, SignedMessageResolverImpl.class).message; + MessageArgument.resolveChatMessage(response, erased, argumentName, (message) -> { + future.complete(message.adventureView()); + }); + return future; + } +} diff --git a/src/main/java/io/papermc/paper/command/brigadier/argument/VanillaArgumentProviderImpl.java b/src/main/java/io/papermc/paper/command/brigadier/argument/VanillaArgumentProviderImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..38fb7d13abfcb55fe4a132b9b27e0c91f8c3d891 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/argument/VanillaArgumentProviderImpl.java @@ -0,0 +1,366 @@ +package io.papermc.paper.command.brigadier.argument; + +import com.destroystokyo.paper.profile.CraftPlayerProfile; +import com.google.common.collect.Collections2; +import com.google.common.collect.Lists; +import com.google.common.collect.Range; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import io.papermc.paper.adventure.PaperAdventure; +import io.papermc.paper.command.brigadier.PaperCommands; +import io.papermc.paper.command.brigadier.argument.predicate.ItemStackPredicate; +import io.papermc.paper.command.brigadier.argument.range.DoubleRangeProvider; +import io.papermc.paper.command.brigadier.argument.range.IntegerRangeProvider; +import io.papermc.paper.command.brigadier.argument.range.RangeProvider; +import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver; +import io.papermc.paper.command.brigadier.argument.resolvers.FinePositionResolver; +import io.papermc.paper.command.brigadier.argument.resolvers.PlayerProfileListResolver; +import io.papermc.paper.command.brigadier.argument.resolvers.selector.EntitySelectorArgumentResolver; +import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver; +import io.papermc.paper.entity.LookAnchor; +import io.papermc.paper.registry.PaperRegistries; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.TypedKey; +import io.papermc.paper.util.MCUtil; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.minecraft.advancements.critereon.MinMaxBounds; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.ColorArgument; +import net.minecraft.commands.arguments.ComponentArgument; +import net.minecraft.commands.arguments.DimensionArgument; +import net.minecraft.commands.arguments.EntityAnchorArgument; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.commands.arguments.GameModeArgument; +import net.minecraft.commands.arguments.GameProfileArgument; +import net.minecraft.commands.arguments.HeightmapTypeArgument; +import net.minecraft.commands.arguments.MessageArgument; +import net.minecraft.commands.arguments.ObjectiveCriteriaArgument; +import net.minecraft.commands.arguments.RangeArgument; +import net.minecraft.commands.arguments.ResourceArgument; +import net.minecraft.commands.arguments.ResourceKeyArgument; +import net.minecraft.commands.arguments.ResourceLocationArgument; +import net.minecraft.commands.arguments.ScoreboardSlotArgument; +import net.minecraft.commands.arguments.StyleArgument; +import net.minecraft.commands.arguments.TemplateMirrorArgument; +import net.minecraft.commands.arguments.TemplateRotationArgument; +import net.minecraft.commands.arguments.TimeArgument; +import net.minecraft.commands.arguments.UuidArgument; +import net.minecraft.commands.arguments.blocks.BlockStateArgument; +import net.minecraft.commands.arguments.coordinates.BlockPosArgument; +import net.minecraft.commands.arguments.coordinates.Vec3Argument; +import net.minecraft.commands.arguments.item.ItemArgument; +import net.minecraft.commands.arguments.item.ItemPredicateArgument; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.bukkit.GameMode; +import org.bukkit.HeightMap; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.block.BlockState; +import org.bukkit.block.structure.Mirror; +import org.bukkit.block.structure.StructureRotation; +import org.bukkit.craftbukkit.CraftHeightMap; +import org.bukkit.craftbukkit.CraftRegistry; +import org.bukkit.craftbukkit.block.CraftBlockStates; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.craftbukkit.scoreboard.CraftCriteria; +import org.bukkit.craftbukkit.scoreboard.CraftScoreboardTranslations; +import org.bukkit.craftbukkit.util.CraftNamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.scoreboard.Criteria; +import org.bukkit.scoreboard.DisplaySlot; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +import static java.util.Objects.requireNonNull; + +@DefaultQualifier(NonNull.class) +public class VanillaArgumentProviderImpl implements VanillaArgumentProvider { + + @Override + public ArgumentType<EntitySelectorArgumentResolver> entity() { + return this.wrap(EntityArgument.entity(), (result) -> sourceStack -> { + return List.of(result.findSingleEntity((CommandSourceStack) sourceStack).getBukkitEntity()); + }); + } + + @Override + public ArgumentType<EntitySelectorArgumentResolver> entities() { + return this.wrap(EntityArgument.entities(), (result) -> sourceStack -> { + return Lists.transform(result.findEntities((CommandSourceStack) sourceStack), net.minecraft.world.entity.Entity::getBukkitEntity); + }); + } + + @Override + public ArgumentType<PlayerSelectorArgumentResolver> player() { + return this.wrap(EntityArgument.player(), (result) -> sourceStack -> { + return List.of(result.findSinglePlayer((CommandSourceStack) sourceStack).getBukkitEntity()); + }); + } + + @Override + public ArgumentType<PlayerSelectorArgumentResolver> players() { + return this.wrap(EntityArgument.players(), (result) -> sourceStack -> { + return Lists.transform(result.findPlayers((CommandSourceStack) sourceStack), ServerPlayer::getBukkitEntity); + }); + } + + @Override + public ArgumentType<PlayerProfileListResolver> playerProfiles() { + return this.wrap(GameProfileArgument.gameProfile(), result -> { + if (result instanceof GameProfileArgument.SelectorResult) { + return sourceStack -> Collections.unmodifiableCollection(Collections2.transform(result.getNames((CommandSourceStack) sourceStack), CraftPlayerProfile::new)); + } else { + return sourceStack -> Collections.unmodifiableCollection(Collections2.transform(result.getNames((CommandSourceStack) sourceStack), CraftPlayerProfile::new)); + } + }); + } + + @Override + public ArgumentType<BlockPositionResolver> blockPosition() { + return this.wrap(BlockPosArgument.blockPos(), (result) -> sourceStack -> { + final BlockPos pos = result.getBlockPos((CommandSourceStack) sourceStack); + + return MCUtil.toPosition(pos); + }); + } + + @Override + public ArgumentType<FinePositionResolver> finePosition(final boolean centerIntegers) { + return this.wrap(Vec3Argument.vec3(centerIntegers), (result) -> sourceStack -> { + final Vec3 vec3 = result.getPosition((CommandSourceStack) sourceStack); + + return MCUtil.toPosition(vec3); + }); + } + + @Override + public ArgumentType<BlockState> blockState() { + return this.wrap(BlockStateArgument.block(PaperCommands.INSTANCE.getBuildContext()), (result) -> { + return CraftBlockStates.getBlockState(CraftRegistry.getMinecraftRegistry(), BlockPos.ZERO, result.getState(), result.tag); + }); + } + + @Override + public ArgumentType<ItemStack> itemStack() { + return this.wrap(ItemArgument.item(PaperCommands.INSTANCE.getBuildContext()), (result) -> { + return CraftItemStack.asBukkitCopy(result.createItemStack(1, true)); + }); + } + + @Override + public ArgumentType<ItemStackPredicate> itemStackPredicate() { + return this.wrap(ItemPredicateArgument.itemPredicate(PaperCommands.INSTANCE.getBuildContext()), type -> itemStack -> type.test(CraftItemStack.asNMSCopy(itemStack))); + } + + @Override + public ArgumentType<NamedTextColor> namedColor() { + return this.wrap(ColorArgument.color(), result -> + requireNonNull( + NamedTextColor.namedColor( + requireNonNull(result.getColor(), () -> result + " didn't have a color") + ), + () -> result.getColor() + " didn't map to an adventure named color" + ) + ); + } + + @Override + public ArgumentType<Component> component() { + return this.wrap(ComponentArgument.textComponent(PaperCommands.INSTANCE.getBuildContext()), PaperAdventure::asAdventure); + } + + @Override + public ArgumentType<Style> style() { + return this.wrap(StyleArgument.style(PaperCommands.INSTANCE.getBuildContext()), PaperAdventure::asAdventure); + } + + @Override + public ArgumentType<SignedMessageResolver> signedMessage() { + return this.wrap(MessageArgument.message(), SignedMessageResolverImpl::new); + } + + @Override + public ArgumentType<DisplaySlot> scoreboardDisplaySlot() { + return this.wrap(ScoreboardSlotArgument.displaySlot(), CraftScoreboardTranslations::toBukkitSlot); + } + + @Override + public ArgumentType<NamespacedKey> namespacedKey() { + return this.wrap(ResourceLocationArgument.id(), CraftNamespacedKey::fromMinecraft); + } + + @Override + public ArgumentType<Key> key() { + return this.wrap(ResourceLocationArgument.id(), CraftNamespacedKey::fromMinecraft); + } + + @Override + public ArgumentType<IntegerRangeProvider> integerRange() { + return this.wrap(RangeArgument.intRange(), type -> VanillaArgumentProviderImpl.convertToRange(type, integerRange -> () -> integerRange)); + } + + @Override + public ArgumentType<DoubleRangeProvider> doubleRange() { + return this.wrap(RangeArgument.floatRange(), type -> VanillaArgumentProviderImpl.convertToRange(type, doubleRange -> () -> doubleRange)); + } + + private static <C extends Number & Comparable<C>, T extends RangeProvider<C>> T convertToRange(final MinMaxBounds<C> bounds, final Function<Range<C>, T> converter) { + if (bounds.isAny()) { + return converter.apply(Range.all()); + } else if (bounds.min().isPresent() && bounds.max().isPresent()) { + return converter.apply(Range.closed(bounds.min().get(), bounds.max().get())); + } else if (bounds.max().isPresent()) { + return converter.apply(Range.atMost(bounds.max().get())); + } else if (bounds.min().isPresent()) { + return converter.apply(Range.atLeast(bounds.min().get())); + } + throw new IllegalStateException("This is a bug: " + bounds); + } + + @Override + public ArgumentType<World> world() { + return this.wrap(DimensionArgument.dimension(), dimensionLocation -> { + // based on DimensionArgument#getDimension + final ResourceKey<Level> resourceKey = ResourceKey.create(Registries.DIMENSION, dimensionLocation); + final @Nullable ServerLevel serverLevel = MinecraftServer.getServer().getLevel(resourceKey); + if (serverLevel == null) { + throw DimensionArgument.ERROR_INVALID_VALUE.create(dimensionLocation); + } else { + return serverLevel.getWorld(); + } + }); + } + + @Override + public ArgumentType<GameMode> gameMode() { + return this.wrap(GameModeArgument.gameMode(), type -> requireNonNull(GameMode.getByValue(type.getId()))); + } + + @Override + public ArgumentType<HeightMap> heightMap() { + return this.wrap(HeightmapTypeArgument.heightmap(), CraftHeightMap::fromNMS); + } + + @Override + public ArgumentType<UUID> uuid() { + return this.wrap(UuidArgument.uuid()); + } + + @Override + public ArgumentType<Criteria> objectiveCriteria() { + return this.wrap(ObjectiveCriteriaArgument.criteria(), CraftCriteria::getFromNMS); + } + + @Override + public ArgumentType<LookAnchor> entityAnchor() { + return this.wrap(EntityAnchorArgument.anchor(), type -> LookAnchor.valueOf(type.name())); + } + + @Override + public ArgumentType<Integer> time(final int minTicks) { + return this.wrap(TimeArgument.time(minTicks)); + } + + @Override + public ArgumentType<Mirror> templateMirror() { + return this.wrap(TemplateMirrorArgument.templateMirror(), mirror -> Mirror.valueOf(mirror.name())); + } + + @Override + public ArgumentType<StructureRotation> templateRotation() { + return this.wrap(TemplateRotationArgument.templateRotation(), mirror -> StructureRotation.valueOf(mirror.name())); + } + + @Override + public <T> ArgumentType<TypedKey<T>> resourceKey(final RegistryKey<T> registryKey) { + return this.wrap( + ResourceKeyArgument.key(PaperRegistries.registryToNms(registryKey)), + nmsRegistryKey -> TypedKey.create(registryKey, CraftNamespacedKey.fromMinecraft(nmsRegistryKey.location())) + ); + } + + @Override + public <T> ArgumentType<T> resource(final RegistryKey<T> registryKey) { + return this.resourceRaw(registryKey); + } + + @SuppressWarnings({"unchecked", "rawtypes", "UnnecessaryLocalVariable"}) + private <T, K extends Keyed> ArgumentType<T> resourceRaw(final RegistryKey registryKeyRaw) { // TODO remove Keyed + final RegistryKey<K> registryKey = registryKeyRaw; + return (ArgumentType<T>) this.wrap( + ResourceArgument.resource(PaperCommands.INSTANCE.getBuildContext(), PaperRegistries.registryToNms(registryKey)), + resource -> requireNonNull( + RegistryAccess.registryAccess() + .getRegistry(registryKey) + .get(CraftNamespacedKey.fromMinecraft(resource.key().location())) + ) + ); + } + + private <T> ArgumentType<T> wrap(final ArgumentType<T> base) { + return this.wrap(base, identity -> identity); + } + + private <B, C> ArgumentType<C> wrap(final ArgumentType<B> base, final ResultConverter<B, C> converter) { + return new NativeWrapperArgumentType<>(base, converter); + } + + @FunctionalInterface + interface ResultConverter<T, R> { + + R convert(T type) throws CommandSyntaxException; + } + + public static final class NativeWrapperArgumentType<M, P> implements ArgumentType<P> { + + private final ArgumentType<M> nmsBase; + private final ResultConverter<M, P> converter; + + private NativeWrapperArgumentType(final ArgumentType<M> nmsBase, final ResultConverter<M, P> converter) { + this.nmsBase = nmsBase; + this.converter = converter; + } + + public ArgumentType<M> nativeNmsArgumentType() { + return this.nmsBase; + } + + @Override + public P parse(final StringReader reader) throws CommandSyntaxException { + return this.converter.convert(this.nmsBase.parse(reader)); + } + + @Override + public <S> CompletableFuture<Suggestions> listSuggestions(final CommandContext<S> context, final SuggestionsBuilder builder) { + return this.nmsBase.listSuggestions(context, builder); + } + + @Override + public Collection<String> getExamples() { + return this.nmsBase.getExamples(); + } + } +} diff --git a/src/main/java/io/papermc/paper/command/brigadier/argument/WrappedArgumentCommandNode.java b/src/main/java/io/papermc/paper/command/brigadier/argument/WrappedArgumentCommandNode.java new file mode 100644 index 0000000000000000000000000000000000000000..c59bbd90fdf04db837366218b312e7fb80366707 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/argument/WrappedArgumentCommandNode.java @@ -0,0 +1,55 @@ +package io.papermc.paper.command.brigadier.argument; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.RedirectModifier; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.context.ParsedArgument; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.CommandNode; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import net.minecraft.commands.synchronization.ArgumentTypeInfos; + +import java.util.function.Predicate; + +/* +Basically this converts the argument to a different type when parsing. + */ +public class WrappedArgumentCommandNode<NMS, API> extends ArgumentCommandNode<CommandSourceStack, NMS> { + + private final ArgumentType<API> pureArgumentType; + + public WrappedArgumentCommandNode( + final String name, + final ArgumentType<API> pureArgumentType, + final ArgumentType<NMS> nmsNativeType, + final Command<CommandSourceStack> command, + final Predicate<CommandSourceStack> requirement, + final CommandNode<CommandSourceStack> redirect, + final RedirectModifier<CommandSourceStack> modifier, + final boolean forks, + final SuggestionProvider<CommandSourceStack> customSuggestions + ) { + super(name, nmsNativeType, command, requirement, redirect, modifier, forks, customSuggestions); + if (!ArgumentTypeInfos.isClassRecognized(nmsNativeType.getClass())) { + // Is this argument an NMS argument? + throw new IllegalArgumentException("Unexpected argument type was passed: " + nmsNativeType.getClass() + ". This should be an NMS type!"); + } + + this.pureArgumentType = pureArgumentType; + } + + // See ArgumentCommandNode#parse + @Override + public void parse(final StringReader reader, final CommandContextBuilder<CommandSourceStack> contextBuilder) throws CommandSyntaxException { + final int start = reader.getCursor(); + final API result = this.pureArgumentType.parse(reader); // Use the api argument parser + final ParsedArgument<CommandSourceStack, API> parsed = new ParsedArgument<>(start, reader.getCursor(), result); // Return an API parsed argument instead. + + contextBuilder.withArgument(this.getName(), parsed); + contextBuilder.withNode(this, parsed.getRange()); + } +} diff --git a/src/main/java/io/papermc/paper/command/brigadier/bukkit/BukkitBrigForwardingMap.java b/src/main/java/io/papermc/paper/command/brigadier/bukkit/BukkitBrigForwardingMap.java new file mode 100644 index 0000000000000000000000000000000000000000..5eef7ae5197bd395fbd6800530ffe34d147651ff --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/bukkit/BukkitBrigForwardingMap.java @@ -0,0 +1,338 @@ +package io.papermc.paper.command.brigadier.bukkit; + +import com.google.common.collect.Iterators; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.tree.CommandNode; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.PaperBrigadier; +import io.papermc.paper.command.brigadier.PaperCommands; +import io.papermc.paper.command.brigadier.PluginVanillaCommandWrapper; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.bukkit.command.Command; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/* +This map is supposed to act as a legacy bridge for the command map and the command dispatcher. + */ +public class BukkitBrigForwardingMap extends HashMap<String, Command> { + + public static BukkitBrigForwardingMap INSTANCE = new BukkitBrigForwardingMap(); + + private final EntrySet entrySet = new EntrySet(); + private final KeySet keySet = new KeySet(); + private final Values values = new Values(); + + // Previous dispatcher used to get commands to migrate to another dispatcher + + public CommandDispatcher<CommandSourceStack> getDispatcher() { + return PaperCommands.INSTANCE.getDispatcherInternal(); + } + + @Override + public int size() { + return this.getDispatcher().getRoot().getChildren().size(); + } + + @Override + public boolean isEmpty() { + return this.size() != 0; + } + + @Override + public boolean containsKey(Object key) { + if (!(key instanceof String stringKey)) { + return false; + } + + // Do any children match? + return this.getDispatcher().getRoot().getChild(stringKey) != null; + } + + @Override + public boolean containsValue(@Nullable final Object value) { + if (!(value instanceof Command)) { + return false; + } + + for (CommandNode<CommandSourceStack> child : this.getDispatcher().getRoot().getChildren()) { + // If child is a bukkit command node, we can convert it! + if (child instanceof BukkitCommandNode bukkitCommandNode) { + return bukkitCommandNode.getBukkitCommand().equals(value); + } + } + + return false; + } + + @Override + public Command get(Object key) { + CommandNode<?> node = this.getDispatcher().getRoot().getChild((String) key); + if (node == null) { + return null; + } + + if (node instanceof BukkitCommandNode bukkitCommandNode) { + return bukkitCommandNode.getBukkitCommand(); + } + + return PaperBrigadier.wrapNode(node); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Nullable + @Override + public Command put(String key, Command value) { + Command old = this.get(key); + this.getDispatcher().getRoot().removeCommand(key); // Override previous command + if (value instanceof PluginVanillaCommandWrapper wrapper && wrapper.getName().equals(key)) { + // Don't break when some plugin tries to remove and add back a plugin command registered with modern API... + this.getDispatcher().getRoot().addChild((CommandNode) wrapper.vanillaCommand); + } else { + this.getDispatcher().getRoot().addChild(BukkitCommandNode.of(key, value)); + } + return old; + } + + @Override + public Command remove(Object key) { + if (!(key instanceof String string)) { + return null; + } + + Command old = this.get(key); + if (old != null) { + this.getDispatcher().getRoot().removeCommand(string); + } + + return old; + } + + @Override + public boolean remove(Object key, Object value) { + Command old = this.get(key); + if (Objects.equals(old, value)) { + this.remove(key); + return true; + } + + return false; + } + + @Override + public void putAll(@NotNull Map<? extends String, ? extends Command> m) { + for (Entry<? extends String, ? extends Command> entry : m.entrySet()) { + this.put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void clear() { + this.getDispatcher().getRoot().clearAll(); + } + + @NotNull + @Override + public Set<String> keySet() { + return this.keySet; + } + + @NotNull + @Override + public Collection<Command> values() { + return this.values; + } + + @NotNull + @Override + public Set<Entry<String, Command>> entrySet() { + return this.entrySet; + } + + final class Values extends AbstractCollection<Command> { + + @Override + public Iterator<Command> iterator() { + // AVOID CME since commands can modify multiple commands now through alises, which means it may appear in the iterator even if removed. + // Oh well! + Iterator<CommandNode<CommandSourceStack>> iterator = new ArrayList<>(BukkitBrigForwardingMap.this.getDispatcher().getRoot().getChildren()).iterator(); + + return new Iterator<>() { + + private CommandNode<CommandSourceStack> lastFetched; + + @Override + public void remove() { + if (this.lastFetched == null) { + throw new IllegalStateException("next not yet called"); + } + + BukkitBrigForwardingMap.this.remove(this.lastFetched.getName()); + iterator.remove(); + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Command next() { + CommandNode<CommandSourceStack> next = iterator.next(); + this.lastFetched = next; + if (next instanceof BukkitCommandNode bukkitCommandNode) { + return bukkitCommandNode.getBukkitCommand(); + } else { + return PaperBrigadier.wrapNode(next); + } + } + }; + } + + @Override + public int size() { + return BukkitBrigForwardingMap.this.getDispatcher().getRoot().getChildren().size(); + } + + @Override + public void clear() { + BukkitBrigForwardingMap.this.clear(); + } + } + + + final class KeySet extends AbstractSet<String> { + + @Override + public int size() { + return BukkitBrigForwardingMap.this.size(); + } + + @Override + public void clear() { + BukkitBrigForwardingMap.this.clear(); + } + + @Override + public Iterator<String> iterator() { + return Iterators.transform(BukkitBrigForwardingMap.this.values.iterator(), Command::getName); // Wrap around the values iterator for consistancy + } + + @Override + public boolean contains(Object o) { + return BukkitBrigForwardingMap.this.containsKey(o); + } + + @Override + public boolean remove(Object o) { + return BukkitBrigForwardingMap.this.remove(o) != null; + } + + @Override + public Spliterator<String> spliterator() { + return this.entryStream().spliterator(); + } + + @Override + public void forEach(Consumer<? super String> action) { + this.entryStream().forEach(action); + } + + private Stream<String> entryStream() { + return BukkitBrigForwardingMap.this.getDispatcher().getRoot().getChildren().stream().map(CommandNode::getName); + } + } + + final class EntrySet extends AbstractSet<Entry<String, Command>> { + @Override + public int size() { + return BukkitBrigForwardingMap.this.size(); + } + + + @Override + public void clear() { + BukkitBrigForwardingMap.this.clear(); + } + + @Override + public Iterator<Entry<String, Command>> iterator() { + return this.entryStream().iterator(); + } + + @Override + public boolean contains(Object o) { + if (!(o instanceof Map.Entry<?, ?> entry)) { + return false; + } + + Object key = entry.getKey(); + Command candidate = get(key); + return candidate != null && candidate.equals(entry.getValue()); + } + + @Override + public boolean remove(Object o) { + if (o instanceof Map.Entry<?, ?> e) { + Object key = e.getKey(); + Object value = e.getValue(); + return BukkitBrigForwardingMap.this.remove(key, value); + } + return false; + } + + @Override + public Spliterator<Entry<String, Command>> spliterator() { + return this.entryStream().spliterator(); + } + + @Override + public void forEach(Consumer<? super Entry<String, Command>> action) { + this.entryStream().forEach(action); + } + + private Stream<Map.Entry<String, Command>> entryStream() { + return BukkitBrigForwardingMap.this.getDispatcher().getRoot().getChildren().stream().map(BukkitBrigForwardingMap.this::nodeToEntry); + } + } + + private Map.Entry<String, Command> nodeToEntry(CommandNode<?> node) { + if (node instanceof BukkitCommandNode bukkitCommandNode) { + return this.mutableEntry(bukkitCommandNode.getName(), bukkitCommandNode.getBukkitCommand()); + } else { + Command wrapped = PaperBrigadier.wrapNode(node); + return this.mutableEntry(node.getName(), wrapped); + } + } + + private Map.Entry<String, Command> mutableEntry(String key, Command command) { + return new Entry<>() { + @Override + public String getKey() { + return key; + } + + @Override + public Command getValue() { + return command; + } + + @Override + public Command setValue(Command value) { + return BukkitBrigForwardingMap.this.put(key, value); + } + }; + } + +} diff --git a/src/main/java/io/papermc/paper/command/brigadier/bukkit/BukkitCommandNode.java b/src/main/java/io/papermc/paper/command/brigadier/bukkit/BukkitCommandNode.java new file mode 100644 index 0000000000000000000000000000000000000000..1814cd072aaca3e72249f0509a9c3b3cb154eaba --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/bukkit/BukkitCommandNode.java @@ -0,0 +1,138 @@ +package io.papermc.paper.command.brigadier.bukkit; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.SuggestionProvider; +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 java.util.ArrayList; +import net.minecraft.commands.CommandSource; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandException; +import org.bukkit.command.CommandSender; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import org.bukkit.entity.Player; +import org.bukkit.event.server.TabCompleteEvent; + +public class BukkitCommandNode extends LiteralCommandNode<CommandSourceStack> { + + private final Command command; + + private BukkitCommandNode(String literal, Command command, BukkitBrigCommand bukkitBrigCommand) { + super( + literal, bukkitBrigCommand, source -> { + // If the source is null, assume it's true. + // As bukkit doesn't really map the command sender well in all cases + if (source instanceof net.minecraft.commands.CommandSourceStack commandSourceStack && commandSourceStack.source == CommandSource.NULL) { + return true; + } else { + return command.testPermissionSilent(source.getSender()); + } + }, + null, null, false + ); + this.command = command; + } + + public static BukkitCommandNode of(String name, Command command) { + BukkitBrigCommand bukkitBrigCommand = new BukkitBrigCommand(command, name); + BukkitCommandNode commandNode = new BukkitCommandNode(name, command, bukkitBrigCommand); + commandNode.addChild( + RequiredArgumentBuilder.<CommandSourceStack, String>argument("args", StringArgumentType.greedyString()) + .suggests(new BukkitBrigSuggestionProvider(command, name)) + .executes(bukkitBrigCommand).build() + ); + + return commandNode; + } + + public Command getBukkitCommand() { + return this.command; + } + + public static class BukkitBrigCommand implements com.mojang.brigadier.Command<CommandSourceStack> { + + private final org.bukkit.command.Command command; + private final String literal; + + BukkitBrigCommand(org.bukkit.command.Command command, String literal) { + this.command = command; + this.literal = literal; + } + + @Override + public int run(CommandContext<CommandSourceStack> context) throws CommandSyntaxException { + CommandSender sender = context.getSource().getSender(); + + String content = context.getRange().get(context.getInput()); + String[] args = org.apache.commons.lang3.StringUtils.split(content, ' '); // fix adjacent spaces (from console/plugins) causing empty array elements + + // Note: we don't return the result of target.execute as thats success / failure, we return handled (true) or not handled (false) + this.command.execute(sender, this.literal, Arrays.copyOfRange(args, 1, args.length)); + + // return true as command was handled + return 1; + } + } + + static class BukkitBrigSuggestionProvider implements SuggestionProvider<CommandSourceStack> { + + private final org.bukkit.command.Command command; + private final String literal; + + BukkitBrigSuggestionProvider(org.bukkit.command.Command command, String literal) { + this.command = command; + this.literal = literal; + } + + @Override + public CompletableFuture<Suggestions> getSuggestions(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) throws CommandSyntaxException { + // Paper start + org.bukkit.command.CommandSender sender = context.getSource().getSender(); + String[] args = builder.getRemaining().split(" ", -1); // We need the command included -- Set limit to -1, allow for trailing spaces + + List<String> results = null; + Location pos = context.getSource().getLocation(); + try { + results = this.command.tabComplete(sender, this.literal, args, pos.clone()); + } catch (CommandException ex) { + sender.sendMessage(ChatColor.RED + "An internal error occurred while attempting to tab-complete this command"); + Bukkit.getServer().getLogger().log(Level.SEVERE, "Exception when " + sender.getName() + " attempted to tab complete " + builder.getRemaining(), ex); + } + + if (sender instanceof final Player player) { + TabCompleteEvent tabEvent = new org.bukkit.event.server.TabCompleteEvent(player, builder.getInput(), results != null ? results : new ArrayList<>(), true, pos); // Paper - AsyncTabCompleteEvent + if (!tabEvent.callEvent()) { + results = null; + } else { + results = tabEvent.getCompletions(); + } + } + // Paper end + if (results == null) { + return builder.buildFuture(); + } + + // Defaults to sub nodes, but we have just one giant args node, so offset accordingly + builder = builder.createOffset(builder.getInput().lastIndexOf(' ') + 1); + + for (String s : results) { + builder.suggest(s); + } + + return builder.buildFuture(); + } + } + +} diff --git a/src/main/java/net/minecraft/commands/CommandSource.java b/src/main/java/net/minecraft/commands/CommandSource.java index 5ba0ef6eda157c4e61d1de99c6b017ceb34430ec..bc5fc57018e347caa5ca453430a45669e086bb22 100644 --- a/src/main/java/net/minecraft/commands/CommandSource.java +++ b/src/main/java/net/minecraft/commands/CommandSource.java @@ -26,7 +26,7 @@ public interface CommandSource { // CraftBukkit start @Override public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) { - throw new UnsupportedOperationException("Not supported yet."); + return io.papermc.paper.brigadier.NullCommandSender.INSTANCE; // Paper - expose a no-op CommandSender } // CraftBukkit end }; diff --git a/src/main/java/net/minecraft/commands/CommandSourceStack.java b/src/main/java/net/minecraft/commands/CommandSourceStack.java index fc0c60b22844ed010aede2fa125b9fa440d3de80..3549ffea451b932602efb113844ba21a7bc72371 100644 --- a/src/main/java/net/minecraft/commands/CommandSourceStack.java +++ b/src/main/java/net/minecraft/commands/CommandSourceStack.java @@ -47,8 +47,7 @@ import net.minecraft.world.phys.Vec2; import net.minecraft.world.phys.Vec3; import com.mojang.brigadier.tree.CommandNode; // CraftBukkit -public class CommandSourceStack implements ExecutionCommandSource<CommandSourceStack>, SharedSuggestionProvider, com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource { // Paper - Brigadier API - +public class CommandSourceStack implements ExecutionCommandSource<CommandSourceStack>, SharedSuggestionProvider, io.papermc.paper.command.brigadier.PaperCommandSourceStack { // Paper - Brigadier API public static final SimpleCommandExceptionType ERROR_NOT_PLAYER = new SimpleCommandExceptionType(Component.translatable("permissions.requires.player")); public static final SimpleCommandExceptionType ERROR_NOT_ENTITY = new SimpleCommandExceptionType(Component.translatable("permissions.requires.entity")); public final CommandSource source; @@ -172,26 +171,6 @@ public class CommandSourceStack implements ExecutionCommandSource<CommandSourceS return this.textName; } - // Paper start - Brigadier API - @Override - public org.bukkit.entity.Entity getBukkitEntity() { - return getEntity() != null ? getEntity().getBukkitEntity() : null; - } - - @Override - public org.bukkit.World getBukkitWorld() { - return getLevel() != null ? getLevel().getWorld() : null; - } - - @Override - public org.bukkit.Location getBukkitLocation() { - Vec3 pos = getPosition(); - org.bukkit.World world = getBukkitWorld(); - Vec2 rot = getRotation(); - return world != null && pos != null ? new org.bukkit.Location(world, pos.x, pos.y, pos.z, rot != null ? rot.y : 0, rot != null ? rot.x : 0) : null; - } - // Paper end - Brigadier API - @Override public boolean hasPermission(int level) { // CraftBukkit start @@ -464,6 +443,12 @@ public class CommandSourceStack implements ExecutionCommandSource<CommandSourceS return this.silent; } + // Paper start + @Override + public CommandSourceStack getHandle() { + return this; + } + // Paper end // CraftBukkit start public org.bukkit.command.CommandSender getBukkitSender() { return this.source.getBukkitSender(this); diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java index cfa328f06c1b972c8328ff40580b485c02e0c270..62704cbfe3a232809b3e62d1f1d8beb3bea6496e 100644 --- a/src/main/java/net/minecraft/commands/Commands.java +++ b/src/main/java/net/minecraft/commands/Commands.java @@ -159,7 +159,7 @@ public class Commands { private final com.mojang.brigadier.CommandDispatcher<CommandSourceStack> dispatcher = new com.mojang.brigadier.CommandDispatcher(); public Commands(Commands.CommandSelection environment, CommandBuildContext commandRegistryAccess) { - this(); // CraftBukkit + // Paper AdvancementCommands.register(this.dispatcher); AttributeCommand.register(this.dispatcher, commandRegistryAccess); ExecuteCommand.register(this.dispatcher, commandRegistryAccess); @@ -268,11 +268,24 @@ public class Commands { } } // Paper end - Vanilla command permission fixes - // CraftBukkit start - } - - public Commands() { - // CraftBukkkit end + // Paper start - Brigadier Command API + // Create legacy minecraft namespace commands + for (final CommandNode<CommandSourceStack> node : new java.util.ArrayList<>(this.dispatcher.getRoot().getChildren())) { + // The brigadier dispatcher is not able to resolve nested redirects. + // E.g. registering the alias minecraft:tp cannot redirect to tp, as tp itself redirects to teleport. + // Instead, target the first none redirecting node. + CommandNode<CommandSourceStack> flattenedAliasTarget = node; + while (flattenedAliasTarget.getRedirect() != null) flattenedAliasTarget = flattenedAliasTarget.getRedirect(); + + this.dispatcher.register( + com.mojang.brigadier.builder.LiteralArgumentBuilder.<CommandSourceStack>literal("minecraft:" + node.getName()) + .executes(flattenedAliasTarget.getCommand()) + .requires(flattenedAliasTarget.getRequirement()) + .redirect(flattenedAliasTarget) + ); + } + // Paper end - Brigadier Command API + // Paper - remove public constructor, no longer needed this.dispatcher.setConsumer(ExecutionCommandSource.resultConsumer()); } @@ -328,6 +341,11 @@ public class Commands { } public void performCommand(ParseResults<CommandSourceStack> parseresults, String s, String label) { // CraftBukkit + // Paper start + this.performCommand(parseresults, s, label, false); + } + public void performCommand(ParseResults<CommandSourceStack> parseresults, String s, String label, boolean throwCommandError) { + // Paper end CommandSourceStack commandlistenerwrapper = (CommandSourceStack) parseresults.getContext().getSource(); Profiler.get().push(() -> { @@ -342,10 +360,11 @@ public class Commands { }); } } catch (Exception exception) { + if (throwCommandError) throw exception; MutableComponent ichatmutablecomponent = Component.literal(exception.getMessage() == null ? exception.getClass().getName() : exception.getMessage()); + Commands.LOGGER.error("Command exception: /{}", s, exception); // Paper - always show execution exception in console log if (commandlistenerwrapper.getServer().isDebugging() || Commands.LOGGER.isDebugEnabled()) { // Paper - Debugging - Commands.LOGGER.error("Command exception: /{}", s, exception); StackTraceElement[] astacktraceelement = exception.getStackTrace(); for (int i = 0; i < Math.min(astacktraceelement.length, 3); ++i) { @@ -479,13 +498,7 @@ public class Commands { private void sendAsync(ServerPlayer player, Collection<CommandNode<CommandSourceStack>> dispatcherRootChildren) { // Paper end - Perf: Async command map building Map<CommandNode<CommandSourceStack>, CommandNode<SharedSuggestionProvider>> map = Maps.newIdentityHashMap(); // Use identity to prevent aliasing issues - RootCommandNode vanillaRoot = new RootCommandNode(); - - RootCommandNode<CommandSourceStack> vanilla = player.server.vanillaCommandDispatcher.getDispatcher().getRoot(); - map.put(vanilla, vanillaRoot); - this.fillUsableCommands(vanilla, vanillaRoot, player.createCommandSourceStack(), (Map) map); - - // Now build the global commands in a second pass + // Paper - brigadier API removes the need to fill the map twice RootCommandNode<SharedSuggestionProvider> rootcommandnode = new RootCommandNode(); map.put(this.dispatcher.getRoot(), rootcommandnode); @@ -520,6 +533,7 @@ public class Commands { // Paper start - Perf: Async command map building; pass copy of children private void fillUsableCommands(Collection<CommandNode<CommandSourceStack>> children, CommandNode<SharedSuggestionProvider> result, CommandSourceStack source, Map<CommandNode<CommandSourceStack>, CommandNode<SharedSuggestionProvider>> resultNodes) { + resultNodes.keySet().removeIf((node) -> !org.spigotmc.SpigotConfig.sendNamespaced && node.getName().contains( ":" )); // Paper - Remove namedspaced from result nodes to prevent redirect trimming ~ see comment below Iterator iterator = children.iterator(); // Paper end - Perf: Async command map building @@ -534,6 +548,42 @@ public class Commands { if (commandnode2.canUse(source)) { ArgumentBuilder argumentbuilder = commandnode2.createBuilder(); // CraftBukkit - decompile error + // Paper start + /* + Because of how commands can be yeeted right left and center due to bad bukkit practices + we need to be able to ensure that ALL commands are registered (even redirects). + + What this will do is IF the redirect seems to be "dead" it will create a builder and essentially populate (flatten) + all the children from the dead redirect to the node. + + So, if minecraft:msg redirects to msg but the original msg node has been overriden minecraft:msg will now act as msg and will explicilty inherit its children. + + The only way to fix this is to either: + - Send EVERYTHING flattened, don't use redirects + - Don't allow command nodes to be deleted + - Do this :) + */ + + // Is there an invalid command redirect? + if (argumentbuilder.getRedirect() != null && (CommandNode) resultNodes.get(argumentbuilder.getRedirect()) == null) { + // Create the argument builder with the same values as the specified node, but with a different literal and populated children + + CommandNode<CommandSourceStack> redirect = argumentbuilder.getRedirect(); + // Diff copied from LiteralCommand#createBuilder + final com.mojang.brigadier.builder.LiteralArgumentBuilder<CommandSourceStack> builder = com.mojang.brigadier.builder.LiteralArgumentBuilder.literal(commandnode2.getName()); + builder.requires(redirect.getRequirement()); + // builder.forward(redirect.getRedirect(), redirect.getRedirectModifier(), redirect.isFork()); We don't want to migrate the forward, since it's invalid. + if (redirect.getCommand() != null) { + builder.executes(redirect.getCommand()); + } + // Diff copied from LiteralCommand#createBuilder + for (CommandNode<CommandSourceStack> child : redirect.getChildren()) { + builder.then(child); + } + + argumentbuilder = builder; + } + // Paper end argumentbuilder.requires((icompletionprovider) -> { return true; diff --git a/src/main/java/net/minecraft/commands/arguments/MessageArgument.java b/src/main/java/net/minecraft/commands/arguments/MessageArgument.java index 55484826fc5ddd04ae024e25a0251796d7fa9c28..237e4f7b24908e9ade9a483eb7ae05fa3b7931d8 100644 --- a/src/main/java/net/minecraft/commands/arguments/MessageArgument.java +++ b/src/main/java/net/minecraft/commands/arguments/MessageArgument.java @@ -40,6 +40,11 @@ public class MessageArgument implements SignedArgument<MessageArgument.Message> public static void resolveChatMessage(CommandContext<CommandSourceStack> context, String name, Consumer<PlayerChatMessage> callback) throws CommandSyntaxException { MessageArgument.Message message = context.getArgument(name, MessageArgument.Message.class); + // Paper start + resolveChatMessage(message, context, name, callback); + } + public static void resolveChatMessage(MessageArgument.Message message, CommandContext<CommandSourceStack> context, String name, Consumer<PlayerChatMessage> callback) throws CommandSyntaxException { + // Paper end CommandSourceStack commandSourceStack = context.getSource(); Component component = message.resolveComponent(commandSourceStack); CommandSigningContext commandSigningContext = commandSourceStack.getSigningContext(); diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 60883d019ceef9e2430e9416c8b321165bba4fe6..c7f84fac5929046d06c38a7624c497dbcda2ff37 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -316,7 +316,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa public static int currentTick; // Paper - improve tick loop public java.util.Queue<Runnable> processQueue = new java.util.concurrent.ConcurrentLinkedQueue<Runnable>(); public int autosavePeriod; - public Commands vanillaCommandDispatcher; + // Paper - don't store the vanilla dispatcher private boolean forceTicks; // CraftBukkit end // Spigot start @@ -406,7 +406,6 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa // CraftBukkit start this.options = options; this.worldLoader = worldLoader; - this.vanillaCommandDispatcher = worldstem.dataPackResources().commands; // CraftBukkit // Paper start - Handled by TerminalConsoleAppender // Try to see if we're actually running in a terminal, disable jline if not /* @@ -691,6 +690,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa this.server.enablePlugins(org.bukkit.plugin.PluginLoadOrder.POSTWORLD); if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins + io.papermc.paper.command.brigadier.PaperCommands.INSTANCE.setValid(); // Paper - reset invalid state for event fire below + io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.callReloadableRegistrarEvent(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS, io.papermc.paper.command.brigadier.PaperCommands.INSTANCE, org.bukkit.plugin.Plugin.class, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.INITIAL); // Paper - call commands event for regular plugins + ((org.bukkit.craftbukkit.help.SimpleHelpMap) this.server.getHelpMap()).initializeCommands(); this.server.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.STARTUP)); this.connection.acceptConnections(); } @@ -2216,9 +2218,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa return new MinecraftServer.ReloadableResources(resourcemanager, datapackresources); }); }).thenAcceptAsync((minecraftserver_reloadableresources) -> { + io.papermc.paper.command.brigadier.PaperBrigadier.moveBukkitCommands(this.resources.managers().getCommands(), minecraftserver_reloadableresources.managers().commands); // Paper this.resources.close(); this.resources = minecraftserver_reloadableresources; - this.server.syncCommands(); // SPIGOT-5884: Lost on reload this.packRepository.setSelected(dataPacks); WorldDataConfiguration worlddataconfiguration = new WorldDataConfiguration(MinecraftServer.getSelectedPacks(this.packRepository, true), this.worldData.enabledFeatures()); @@ -2232,6 +2234,15 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa this.structureTemplateManager.onResourceManagerReload(this.resources.resourceManager); this.fuelValues = FuelValues.vanillaBurnTimes(this.registries.compositeAccess(), this.worldData.enabledFeatures()); org.bukkit.craftbukkit.block.data.CraftBlockData.reloadCache(); // Paper - cache block data strings; they can be defined by datapacks so refresh it here + // Paper start - brigadier command API + io.papermc.paper.command.brigadier.PaperCommands.INSTANCE.setValid(); // reset invalid state for event fire below + io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.callReloadableRegistrarEvent(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS, io.papermc.paper.command.brigadier.PaperCommands.INSTANCE, org.bukkit.plugin.Plugin.class, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.RELOAD); // call commands event for regular plugins + final org.bukkit.craftbukkit.help.SimpleHelpMap helpMap = (org.bukkit.craftbukkit.help.SimpleHelpMap) this.server.getHelpMap(); + helpMap.clear(); + helpMap.initializeGeneralTopics(); + helpMap.initializeCommands(); + this.server.syncCommands(); // Refresh commands after event + // Paper end new io.papermc.paper.event.server.ServerResourcesReloadedEvent(cause).callEvent(); // Paper - Add ServerResourcesReloadedEvent; fire after everything has been reloaded }, this); diff --git a/src/main/java/net/minecraft/server/ReloadableServerResources.java b/src/main/java/net/minecraft/server/ReloadableServerResources.java index ac9a706dd92f15406299b8fc4ed567ffcc736169..47d5d5fcc8623969c6ab7c148c043bc367f1d6cf 100644 --- a/src/main/java/net/minecraft/server/ReloadableServerResources.java +++ b/src/main/java/net/minecraft/server/ReloadableServerResources.java @@ -39,6 +39,7 @@ public class ReloadableServerResources { this.postponedTags = pendingTagLoads; this.recipes = new RecipeManager(registries); this.commands = new Commands(environment, CommandBuildContext.simple(registries, enabledFeatures)); + io.papermc.paper.command.brigadier.PaperCommands.INSTANCE.setDispatcher(this.commands, CommandBuildContext.simple(registries, enabledFeatures)); // Paper - Brigadier Command API this.advancements = new ServerAdvancementManager(registries); this.functionLibrary = new ServerFunctionLibrary(functionPermissionLevel, this.commands.getDispatcher()); } @@ -83,6 +84,14 @@ public class ReloadableServerResources { ReloadableServerResources reloadableServerResources = new ReloadableServerResources( reloadResult.layers(), reloadResult.lookupWithUpdatedTags(), enabledFeatures, environment, pendingTagLoads, functionPermissionLevel ); + // Paper start - call commands event for bootstraps + //noinspection ConstantValue + io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.callReloadableRegistrarEvent( + io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS, + io.papermc.paper.command.brigadier.PaperCommands.INSTANCE, + io.papermc.paper.plugin.bootstrap.BootstrapContext.class, + MinecraftServer.getServer() == null ? io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.INITIAL : io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.RELOAD); + // Paper end - call commands event return SimpleReloadInstance.create( resourceManager, reloadableServerResources.listeners(), diff --git a/src/main/java/net/minecraft/server/ServerFunctionManager.java b/src/main/java/net/minecraft/server/ServerFunctionManager.java index 02e00819970eda49196641520870fc31d08b1a38..0b348f701b61c7b7ed0190eff8b2d73f3a3d5c74 100644 --- a/src/main/java/net/minecraft/server/ServerFunctionManager.java +++ b/src/main/java/net/minecraft/server/ServerFunctionManager.java @@ -37,7 +37,7 @@ public class ServerFunctionManager { } public CommandDispatcher<CommandSourceStack> getDispatcher() { - return this.server.vanillaCommandDispatcher.getDispatcher(); // CraftBukkit + return this.server.getCommands().getDispatcher(); // CraftBukkit // Paper - Don't override command dispatcher } public void tick() { diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java index ebe6a002d883721d80cbfcc004064e8a57934a56..cce0e570c8217c8e7cc81642d303e1b96f70f4f3 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java @@ -234,7 +234,6 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now - io.papermc.paper.brigadier.PaperBrigadierProviderImpl.INSTANCE.getClass(); // Paper - init PaperBrigadierProvider this.setPvpAllowed(dedicatedserverproperties.pvp); this.setFlightAllowed(dedicatedserverproperties.allowFlight); diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java index a13c199446423dbc807f916157bb7cac33229389..929cd59b8f27d7b708054d80e764a11d6551f060 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -2489,30 +2489,20 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl } } - private void handleCommand(String s) { - org.spigotmc.AsyncCatcher.catchOp("Command Dispatched Async: " + s); // Paper - Add async catcher - if ( org.spigotmc.SpigotConfig.logCommands ) // Spigot - this.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + s); - - CraftPlayer player = this.getCraftPlayer(); - - PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(player, s, new LazyPlayerSet(this.server)); - this.cserver.getPluginManager().callEvent(event); - - if (event.isCancelled()) { - return; - } - - try { - if (this.cserver.dispatchCommand(event.getPlayer(), event.getMessage().substring(1))) { - return; - } - } catch (org.bukkit.command.CommandException ex) { - player.sendMessage(org.bukkit.ChatColor.RED + "An internal error occurred while attempting to perform this command"); - java.util.logging.Logger.getLogger(ServerGamePacketListenerImpl.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); - return; - } finally { + @Deprecated // Paper + public void handleCommand(String s) { // Paper - private -> public + // Paper start - Remove all this old duplicated logic + if (s.startsWith("/")) { + s = s.substring(1); } + /* + It should be noted that this represents the "legacy" command execution path. + Api can call commands even if there is no additional context provided. + This method should ONLY be used if you need to execute a command WITHOUT + an actual player's input. + */ + this.performUnsignedChatCommand(s); + // Paper end } // CraftBukkit end diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index 0d62694e2f9086702eaca7a11799eb90a06ce853..338b60f0254d55ac4a0645ca351d0ce736ce0681 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -275,11 +275,11 @@ public final class CraftServer implements Server { private final Logger logger = Logger.getLogger("Minecraft"); private final ServicesManager servicesManager = new SimpleServicesManager(); private final CraftScheduler scheduler = new CraftScheduler(); - private final CraftCommandMap commandMap = new CraftCommandMap(this); + private final CraftCommandMap commandMap; // Paper - Move down private final SimpleHelpMap helpMap = new SimpleHelpMap(this); private final StandardMessenger messenger = new StandardMessenger(); - 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 SimplePluginManager pluginManager; // Paper - Move down + public final io.papermc.paper.plugin.manager.PaperPluginManagerImpl paperPluginManager; // Paper private final StructureManager structureManager; protected final DedicatedServer console; protected final DedicatedPlayerList playerList; @@ -419,6 +419,12 @@ public final class CraftServer implements Server { this.serverLinks = new CraftServerLinks(console); Bukkit.setServer(this); + // Paper start + this.commandMap = new CraftCommandMap(this); + this.pluginManager = new SimplePluginManager(this, commandMap); + this.paperPluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(this, this.commandMap, pluginManager); + this.pluginManager.paperPluginManager = this.paperPluginManager; + // Paper end CraftRegistry.setMinecraftRegistry(console.registryAccess()); @@ -617,48 +623,11 @@ public final class CraftServer implements Server { } private void setVanillaCommands(boolean first) { // Spigot - Commands dispatcher = this.console.vanillaCommandDispatcher; - - // Build a list of all Vanilla commands and create wrappers - for (CommandNode<CommandSourceStack> cmd : dispatcher.getDispatcher().getRoot().getChildren()) { - // Spigot start - VanillaCommandWrapper wrapper = new VanillaCommandWrapper(dispatcher, cmd); - if (org.spigotmc.SpigotConfig.replaceCommands.contains( wrapper.getName() ) ) { - if (first) { - this.commandMap.register("minecraft", wrapper); - } - } else if (!first) { - this.commandMap.register("minecraft", wrapper); - } - // Spigot end - } + // Paper - Replace implementation } public void syncCommands() { - // Clear existing commands - Commands dispatcher = this.console.resources.managers().commands = new Commands(); - - // Register all commands, vanilla ones will be using the old dispatcher references - for (Map.Entry<String, Command> entry : this.commandMap.getKnownCommands().entrySet()) { - String label = entry.getKey(); - Command command = entry.getValue(); - - if (command instanceof VanillaCommandWrapper) { - LiteralCommandNode<CommandSourceStack> node = (LiteralCommandNode<CommandSourceStack>) ((VanillaCommandWrapper) command).vanillaCommand; - if (!node.getLiteral().equals(label)) { - LiteralCommandNode<CommandSourceStack> clone = new LiteralCommandNode(label, node.getCommand(), node.getRequirement(), node.getRedirect(), node.getRedirectModifier(), node.isFork()); - - for (CommandNode<CommandSourceStack> child : node.getChildren()) { - clone.addChild(child); - } - node = clone; - } - - dispatcher.getDispatcher().getRoot().addChild(node); - } else { - new BukkitCommandWrapper(this, entry.getValue()).register(dispatcher.getDispatcher(), label); - } - } + Commands dispatcher = this.getHandle().getServer().getCommands(); // Paper - We now register directly to the dispatcher. // Refresh commands for (ServerPlayer player : this.getHandle().players) { @@ -1045,17 +1014,31 @@ public final class CraftServer implements Server { return true; } - // Spigot start - if (!org.spigotmc.SpigotConfig.unknownCommandMessage.isEmpty()) { - // Paper start - org.bukkit.event.command.UnknownCommandEvent event = new org.bukkit.event.command.UnknownCommandEvent(sender, commandLine, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.unknownCommandMessage)); - this.getPluginManager().callEvent(event); - if (event.message() != null) { - sender.sendMessage(event.message()); - } - // Paper end + return this.dispatchCommand(VanillaCommandWrapper.getListener(sender), commandLine); + } + + public boolean dispatchCommand(CommandSourceStack sourceStack, String commandLine) { + net.minecraft.commands.Commands commands = this.getHandle().getServer().getCommands(); + com.mojang.brigadier.CommandDispatcher<CommandSourceStack> dispatcher = commands.getDispatcher(); + com.mojang.brigadier.ParseResults<CommandSourceStack> results = dispatcher.parse(commandLine, sourceStack); + + CommandSender sender = sourceStack.getBukkitSender(); + String[] args = org.apache.commons.lang3.StringUtils.split(commandLine, ' '); // Paper - fix adjacent spaces (from console/plugins) causing empty array elements + Command target = this.commandMap.getCommand(args[0].toLowerCase(java.util.Locale.ENGLISH)); + + try { + commands.performCommand(results, commandLine, commandLine, true); + } catch (CommandException ex) { + this.pluginManager.callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerCommandException(ex, target, sender, args))); // Paper + //target.timings.stopTiming(); // Spigot // Paper + throw ex; + } catch (Throwable ex) { + //target.timings.stopTiming(); // Spigot // Paper + String msg = "Unhandled exception executing '" + commandLine + "' in " + target; + this.pluginManager.callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerCommandException(ex, target, sender, args))); // Paper + throw new CommandException(msg, ex); } - // Spigot end + // Paper end return false; } @@ -1064,7 +1047,7 @@ public final class CraftServer implements Server { public void reload() { // Paper start - lifecycle events if (io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.blocksPluginReloading()) { - throw new IllegalStateException("A lifecycle event handler has been registered which makes reloading plugins not possible"); + throw new IllegalStateException(org.bukkit.command.defaults.ReloadCommand.RELOADING_DISABLED_MESSAGE); } // Paper end - lifecycle events org.spigotmc.WatchdogThread.hasStarted = false; // Paper - Disable watchdog early timeout on reload @@ -1119,8 +1102,9 @@ public final class CraftServer implements Server { } Plugin[] pluginClone = pluginManager.getPlugins().clone(); // Paper + this.commandMap.clearCommands(); // Paper - Move command reloading up this.pluginManager.clearPlugins(); - this.commandMap.clearCommands(); + // Paper - move up // Paper start for (Plugin plugin : pluginClone) { entityMetadata.removeAll(plugin); @@ -1160,6 +1144,12 @@ public final class CraftServer implements Server { this.enablePlugins(PluginLoadOrder.STARTUP); this.enablePlugins(PluginLoadOrder.POSTWORLD); if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins + // Paper start - brigadier command API + io.papermc.paper.command.brigadier.PaperCommands.INSTANCE.setValid(); // to clear invalid state for event fire below + io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.callReloadableRegistrarEvent(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS, io.papermc.paper.command.brigadier.PaperCommands.INSTANCE, org.bukkit.plugin.Plugin.class, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.RELOAD); // call commands event for regular plugins + this.helpMap.initializeCommands(); + this.syncCommands(); // Refresh commands after event + // Paper end - brigadier command API this.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.RELOAD)); org.spigotmc.WatchdogThread.hasStarted = true; // Paper - Disable watchdog early timeout on reload } diff --git a/src/main/java/org/bukkit/craftbukkit/command/BukkitCommandWrapper.java b/src/main/java/org/bukkit/craftbukkit/command/BukkitCommandWrapper.java index 21b6f90cf5bd7087d1a0f512289d971f2c3e1afa..a3c02200da9e793de79a74fe7e0cd72634150f64 100644 --- a/src/main/java/org/bukkit/craftbukkit/command/BukkitCommandWrapper.java +++ b/src/main/java/org/bukkit/craftbukkit/command/BukkitCommandWrapper.java @@ -20,6 +20,7 @@ import org.bukkit.command.CommandException; import org.bukkit.command.CommandSender; import org.bukkit.craftbukkit.CraftServer; +@Deprecated(forRemoval = true) // Paper - Don't use public class BukkitCommandWrapper implements com.mojang.brigadier.Command<CommandSourceStack>, Predicate<CommandSourceStack>, SuggestionProvider<CommandSourceStack>, com.destroystokyo.paper.brigadier.BukkitBrigadierCommand<CommandSourceStack> { // Paper private final CraftServer server; diff --git a/src/main/java/org/bukkit/craftbukkit/command/CraftCommandMap.java b/src/main/java/org/bukkit/craftbukkit/command/CraftCommandMap.java index 4b1ac1fe7ea07f419ae2818251900e7ba434ee16..90ed57a7fbcd0625b64084347460e9864216f610 100644 --- a/src/main/java/org/bukkit/craftbukkit/command/CraftCommandMap.java +++ b/src/main/java/org/bukkit/craftbukkit/command/CraftCommandMap.java @@ -8,7 +8,7 @@ import org.bukkit.command.SimpleCommandMap; public class CraftCommandMap extends SimpleCommandMap { public CraftCommandMap(Server server) { - super(server); + super(server, io.papermc.paper.command.brigadier.bukkit.BukkitBrigForwardingMap.INSTANCE); } public Map<String, Command> getKnownCommands() { diff --git a/src/main/java/org/bukkit/craftbukkit/command/VanillaCommandWrapper.java b/src/main/java/org/bukkit/craftbukkit/command/VanillaCommandWrapper.java index ce8683eff5b8ade57a2fcb77027cfe4b26986bc7..68001fbc7abf4c87740ebe8e7e492fa254be0da1 100644 --- a/src/main/java/org/bukkit/craftbukkit/command/VanillaCommandWrapper.java +++ b/src/main/java/org/bukkit/craftbukkit/command/VanillaCommandWrapper.java @@ -24,14 +24,26 @@ import org.bukkit.craftbukkit.entity.CraftMinecartCommand; import org.bukkit.craftbukkit.entity.CraftPlayer; import org.bukkit.entity.minecart.CommandMinecart; -public final class VanillaCommandWrapper extends BukkitCommand { +public class VanillaCommandWrapper extends BukkitCommand { // Paper - private final Commands dispatcher; + //private final Commands dispatcher; // Paper public final CommandNode<CommandSourceStack> vanillaCommand; + // Paper start + public VanillaCommandWrapper(String name, String description, String usageMessage, List<String> aliases, CommandNode<CommandSourceStack> vanillaCommand) { + super(name, description, usageMessage, aliases); + //this.dispatcher = dispatcher; // Paper + this.vanillaCommand = vanillaCommand; + } + + Commands commands() { + return net.minecraft.server.MinecraftServer.getServer().getCommands(); + } + + // Paper end public VanillaCommandWrapper(Commands dispatcher, CommandNode<CommandSourceStack> vanillaCommand) { super(vanillaCommand.getName(), "A Mojang provided command.", vanillaCommand.getUsageText(), Collections.EMPTY_LIST); - this.dispatcher = dispatcher; + // this.dispatcher = dispatcher; // Paper this.vanillaCommand = vanillaCommand; this.setPermission(VanillaCommandWrapper.getPermission(vanillaCommand)); } @@ -41,7 +53,7 @@ public final class VanillaCommandWrapper extends BukkitCommand { if (!this.testPermission(sender)) return true; CommandSourceStack icommandlistener = VanillaCommandWrapper.getListener(sender); - this.dispatcher.performPrefixedCommand(icommandlistener, this.toDispatcher(args, this.getName()), this.toDispatcher(args, commandLabel)); + this.commands().performPrefixedCommand(icommandlistener, this.toDispatcher(args, this.getName()), this.toDispatcher(args, commandLabel)); // Paper return true; } @@ -52,10 +64,10 @@ public final class VanillaCommandWrapper extends BukkitCommand { Preconditions.checkArgument(alias != null, "Alias cannot be null"); CommandSourceStack icommandlistener = VanillaCommandWrapper.getListener(sender); - ParseResults<CommandSourceStack> parsed = this.dispatcher.getDispatcher().parse(this.toDispatcher(args, this.getName()), icommandlistener); + ParseResults<CommandSourceStack> parsed = this.commands().getDispatcher().parse(this.toDispatcher(args, this.getName()), icommandlistener); // Paper List<String> results = new ArrayList<>(); - this.dispatcher.getDispatcher().getCompletionSuggestions(parsed).thenAccept((suggestions) -> { + this.commands().getDispatcher().getCompletionSuggestions(parsed).thenAccept((suggestions) -> { // Paper suggestions.getList().forEach((s) -> results.add(s.getText())); }); @@ -116,4 +128,15 @@ public final class VanillaCommandWrapper extends BukkitCommand { private String toDispatcher(String[] args, String name) { return name + ((args.length > 0) ? " " + Joiner.on(' ').join(args) : ""); } + // Paper start + @Override + public boolean canBeOverriden() { + return true; + } + + @Override + public boolean isRegistered() { + return true; + } + // Paper end } diff --git a/src/main/java/org/bukkit/craftbukkit/help/SimpleHelpMap.java b/src/main/java/org/bukkit/craftbukkit/help/SimpleHelpMap.java index 05d3aecd4abaab6a94effcb1ab35c1b82410865f..97141968f36b3ef88bd6e520c2ccc37c97e4adb1 100644 --- a/src/main/java/org/bukkit/craftbukkit/help/SimpleHelpMap.java +++ b/src/main/java/org/bukkit/craftbukkit/help/SimpleHelpMap.java @@ -200,15 +200,18 @@ public class SimpleHelpMap implements HelpMap { } private String getCommandPluginName(Command command) { + // Paper start - Move up + if (command instanceof PluginIdentifiableCommand) { + return ((PluginIdentifiableCommand) command).getPlugin().getName(); + } + // Paper end if (command instanceof VanillaCommandWrapper) { return "Minecraft"; } if (command instanceof BukkitCommand) { return "Bukkit"; } - if (command instanceof PluginIdentifiableCommand) { - return ((PluginIdentifiableCommand) command).getPlugin().getName(); - } + // Paper - Move PluginIdentifiableCommand instanceof check to allow brig commands return null; } diff --git a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftCriteria.java b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftCriteria.java index 8464531a4ee400834d25c23b1eb723f49be8689e..4a0b1587180381123eb843819cd10630e49c7a02 100644 --- a/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftCriteria.java +++ b/src/main/java/org/bukkit/craftbukkit/scoreboard/CraftCriteria.java @@ -53,7 +53,13 @@ public final class CraftCriteria implements Criteria { return RenderType.values()[this.criteria.getDefaultRenderType().ordinal()]; } - static CraftCriteria getFromNMS(Objective objective) { + // Paper start + public static CraftCriteria getFromNMS(ObjectiveCriteria criteria) { + return java.util.Objects.requireNonNullElseGet(CraftCriteria.DEFAULTS.get(criteria.getName()), () -> new CraftCriteria(criteria)); + } + // Paper end + + public static CraftCriteria getFromNMS(Objective objective) { return java.util.Objects.requireNonNullElseGet(CraftCriteria.DEFAULTS.get(objective.getCriteria().getName()), () -> new CraftCriteria(objective.getCriteria())); // Paper } diff --git a/src/main/resources/META-INF/services/io.papermc.paper.command.brigadier.CommandBuilderProvider b/src/main/resources/META-INF/services/io.papermc.paper.command.brigadier.CommandBuilderProvider new file mode 100644 index 0000000000000000000000000000000000000000..2f0b1f0ed9ca3605cd24a75466973e1a0a745ee5 --- /dev/null +++ b/src/main/resources/META-INF/services/io.papermc.paper.command.brigadier.CommandBuilderProvider @@ -0,0 +1 @@ +io.papermc.paper.command.brigadier.CommandBuilderImpl$ProviderImpl diff --git a/src/main/resources/META-INF/services/io.papermc.paper.command.brigadier.MessageComponentSerializer b/src/main/resources/META-INF/services/io.papermc.paper.command.brigadier.MessageComponentSerializer new file mode 100644 index 0000000000000000000000000000000000000000..2428b577b9bf0eac6947f5d919cbb51f7aca3d50 --- /dev/null +++ b/src/main/resources/META-INF/services/io.papermc.paper.command.brigadier.MessageComponentSerializer @@ -0,0 +1 @@ +io.papermc.paper.command.brigadier.MessageComponentSerializerImpl diff --git a/src/main/resources/META-INF/services/io.papermc.paper.command.brigadier.argument.VanillaArgumentProvider b/src/main/resources/META-INF/services/io.papermc.paper.command.brigadier.argument.VanillaArgumentProvider new file mode 100644 index 0000000000000000000000000000000000000000..b2fdb8351c2abb55283850a929d2a87aa6ecb80f --- /dev/null +++ b/src/main/resources/META-INF/services/io.papermc.paper.command.brigadier.argument.VanillaArgumentProvider @@ -0,0 +1 @@ +io.papermc.paper.command.brigadier.argument.VanillaArgumentProviderImpl diff --git a/src/test/java/io/papermc/paper/command/brigadier/BukkitCommandConversionTest.java b/src/test/java/io/papermc/paper/command/brigadier/BukkitCommandConversionTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4b419ce023f61d5af9ff7a34e6879de1991cf4df --- /dev/null +++ b/src/test/java/io/papermc/paper/command/brigadier/BukkitCommandConversionTest.java @@ -0,0 +1,102 @@ +package io.papermc.paper.command.brigadier; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import io.papermc.paper.command.brigadier.bukkit.BukkitBrigForwardingMap; +import java.util.List; +import java.util.Map; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandMap; +import org.bukkit.command.CommandSender; +import org.bukkit.command.SimpleCommandMap; +import org.bukkit.support.RegistryHelper; +import org.bukkit.support.environment.AllFeatures; +import org.bukkit.support.environment.Normal; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +@Normal +public class BukkitCommandConversionTest { + + private CommandSender getSender() { + return Mockito.mock(CommandSender.class); + } + + @Test + public void test() throws CommandSyntaxException { + CommandSender sender = this.getSender(); + CommandSourceStack object = Mockito.mock(CommandSourceStack.class); + Mockito.when(object.getLocation()).thenReturn(new Location(null, 0, 0, 0));; + + CommandDispatcher dispatcher = RegistryHelper.getDataPack().commands.getDispatcher(); + dispatcher.setConsumer((context, success, result) -> {}); + CommandMap commandMap = new SimpleCommandMap(Bukkit.getServer(), new BukkitBrigForwardingMap()); + Map<String, Command> stringCommandMap = commandMap.getKnownCommands(); + // All commands should be mirrored -- or equal + int commandMapSize = stringCommandMap.values().size(); + ExampleCommand exampleCommand = new ExampleCommand(); + + Assertions.assertEquals(commandMapSize, dispatcher.getRoot().getChildren().size()); + + // Register a new command + commandMap.register("test", exampleCommand); + Assertions.assertEquals(commandMapSize + (3 * 2), stringCommandMap.values().size()); // Make sure commands are accounted for, including those with namespaced keys + + // Test Registration + for (String alias : exampleCommand.getAliases()) { + Assertions.assertEquals(stringCommandMap.get(alias), exampleCommand); + Assertions.assertEquals(stringCommandMap.get("test:" + alias), exampleCommand); + } + // Test command instance equality + Assertions.assertEquals(stringCommandMap.get(exampleCommand.getName()), exampleCommand); + Assertions.assertEquals(stringCommandMap.get("test:" + exampleCommand.getName()), exampleCommand); + + // Test command map execution + commandMap.dispatch(sender, "main-example example"); + Assertions.assertEquals(exampleCommand.invocations, 1); + Assertions.assertEquals(commandMap.tabComplete(sender, "main-example 1 2"), List.of("complete")); + + // Test dispatcher execution + dispatcher.execute("main-example example", object); + Assertions.assertEquals(exampleCommand.invocations, 2); + + dispatcher.execute("test:example2 example", object); + Assertions.assertEquals(exampleCommand.invocations, 3); + + Suggestions suggestions = (Suggestions) dispatcher.getCompletionSuggestions(dispatcher.parse("main-example 1 2", object)).join(); + Assertions.assertEquals(suggestions.getList().get(0).getText(), "complete"); + + + // Test command map removal + commandMap.getKnownCommands().remove("test"); + Assertions.assertNull(commandMap.getCommand("test")); + Assertions.assertNull(dispatcher.getRoot().getChild("test")); + } + + private static class ExampleCommand extends Command { + + int invocations; + + protected ExampleCommand() { + super("main-example", "This is an example.", "", List.of("example", "example2")); + } + + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { + Assertions.assertEquals(args[0], "example"); + this.invocations++; + return true; + } + + @Override + public @NotNull List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException { + Assertions.assertEquals(args.length, 2); + return List.of("complete"); + } + } +} diff --git a/src/test/java/org/bukkit/support/DummyServerHelper.java b/src/test/java/org/bukkit/support/DummyServerHelper.java index 2fed099bc91a8591a2415493b333f9c18bfe35f6..55c05c16a80c489cdda2fd03c943921d38d978e9 100644 --- a/src/test/java/org/bukkit/support/DummyServerHelper.java +++ b/src/test/java/org/bukkit/support/DummyServerHelper.java @@ -87,7 +87,7 @@ public final class DummyServerHelper { // Paper start - testing additions final Thread currentThread = Thread.currentThread(); when(instance.isPrimaryThread()).thenAnswer(ignored -> Thread.currentThread().equals(currentThread)); - final org.bukkit.plugin.PluginManager pluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(instance, new org.bukkit.command.SimpleCommandMap(instance), null); + final org.bukkit.plugin.PluginManager pluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(instance, new org.bukkit.command.SimpleCommandMap(instance, new java.util.HashMap<>()), null); when(instance.getPluginManager()).thenReturn(pluginManager); // Paper end - testing additions