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 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 { } private String getSmartUsage(final CommandNode 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 { final String redirect = node.getRedirect() == this.root ? "..." : "-> " + node.getRedirect().getUsageText(); return self + CommandDispatcher.ARGUMENT_SEPARATOR + redirect; } else { - final Collection> children = node.getChildren().stream().filter(c -> c.canUse(source)).collect(Collectors.toList()); + final Collection> 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 implements Comparable> { private final boolean forks; private Command command; public CommandNode clientNode; // Paper - Brigadier API + public CommandNode unwrappedCached = null; // Paper - Brigadier Command API + public CommandNode 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 implements Comparable> { } public abstract Collection 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 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..2b1cf02f32506187ed0783ae4732ae0f0bdbfc0e --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/ApiMirrorRootNode.java @@ -0,0 +1,252 @@ +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. + *

+ * 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. + *

+ * 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 { + + /** + * 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>> 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 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 unwrapNode(final CommandNode 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 convertFromPureBrigNode(final CommandNode pureNode) { + /* + Logic for converting a node. + */ + final CommandNode converted; + if (pureNode instanceof final LiteralCommandNode 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 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."); + } + + /* + Add the children to the node, unwrapping each child in the process. + */ + for (final CommandNode child : pureNode.getChildren()) { + converted.addChild(this.unwrapNode(child)); + } + + converted.wrappedCached = pureNode; + pureNode.unwrappedCached = converted; + + 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. + *

+ * 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 wrapNode(@Nullable final CommandNode 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 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 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> getChildren() { + return Collections2.transform(this.getDispatcher().getRoot().getChildren(), this::wrapNode); + } + + @Override + public CommandNode 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 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 simpleUnwrap(final CommandNode 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 argumentCommandNode = node; + if (argumentCommandNode.getRedirect() != null) { + argumentCommandNode = argumentCommandNode.getRedirect(); + } + + Map, 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) 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..27509813a90980be1dfc7bde27d0eba60adfc820 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/PaperCommands.java @@ -0,0 +1,193 @@ +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.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.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 { + + public static final PaperCommands INSTANCE = new PaperCommands(); + + private @Nullable LifecycleEventOwner currentContext; + private @MonotonicNonNull CommandDispatcher 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 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 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 getDispatcher() { + Preconditions.checkState(!this.invalid && this.dispatcher != null, "cannot access the dispatcher in this context"); + return this.dispatcher; + } + + @Override + public @Unmodifiable Set register(final LiteralCommandNode node, final @Nullable String description, final Collection aliases) { + return this.register(requireNonNull(this.currentContext, "No lifecycle owner context is set").getPluginMeta(), node, description, aliases); + } + + @Override + public @Unmodifiable Set register(final PluginMeta pluginMeta, final LiteralCommandNode node, final @Nullable String description, final Collection aliases) { + return this.registerWithFlags(pluginMeta, node, description, aliases, Set.of()); + } + + @Override + public @Unmodifiable Set registerWithFlags(@NotNull final PluginMeta pluginMeta, @NotNull final LiteralCommandNode node, @org.jetbrains.annotations.Nullable final String description, @NotNull final Collection aliases, @NotNull final Set 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 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 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 redirect; + if (redirectTo.getChildren().isEmpty() || hasFlattenRedirectFlag) { + redirect = Commands.literal(aliasLiteral) + .executes(redirectTo.getCommand()) + .requires(redirectTo.getRequirement()) + .build(); + + for (final CommandNode 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, final boolean override) { + final boolean hasChild = this.getDispatcher().getRoot().getChild(node.getLiteral()) != null; + if (!hasChild || 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 register(final String label, final @Nullable String description, final Collection 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 register(final PluginMeta pluginMeta, final String label, final @Nullable String description, final Collection aliases, final BasicCommand basicCommand) { + final LiteralArgumentBuilder builder = Commands.literal(label) + .then( + Commands.argument("args", StringArgumentType.greedyString()) + .suggests((context, suggestionsBuilder) -> { + final String[] args = StringUtils.split(suggestionsBuilder.getRemaining()); + final SuggestionsBuilder offsetSuggestionsBuilder = suggestionsBuilder.createOffset(suggestionsBuilder.getInput().lastIndexOf(' ') + 1);; + + final Collection 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 { + + private final PluginMeta plugin; + private final String description; + private List aliases = Collections.emptyList(); + + public PluginCommandNode(final @NotNull String literal, final @NotNull PluginMeta plugin, final @NotNull LiteralCommandNode 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 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 aliases) { + this.aliases = aliases; + } + + public List 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 alises; + + public PluginVanillaCommandWrapper(String name, String description, String usageMessage, List aliases, CommandNode vanillaCommand, Plugin plugin) { + super(name, description, usageMessage, aliases, vanillaCommand); + this.plugin = plugin; + this.alises = aliases; + } + + @Override + public @NotNull List getAliases() { + return this.alises; + } + + @Override + public @NotNull Command setAliases(@NotNull List 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 { + + private final CommandNode handle; + + public ShadowBrigNode(CommandNode 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> getChildren() { + throw new UnsupportedOperationException("Cannot retrieve children from this node."); + } + + @Override + public CommandNode getChild(String name) { + throw new UnsupportedOperationException("Cannot retrieve children from this node."); + } + + @Override + public void addChild(CommandNode node) { + throw new UnsupportedOperationException("Cannot modify children for this node."); + } + + public CommandNode 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..72966584089d3fee9778f572727c9b7f4a4d4302 --- /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 net.kyori.adventure.chat.SignedMessage; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.arguments.MessageArgument; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CompletableFuture; + +public record SignedMessageResolverImpl(MessageArgument.Message message) implements SignedMessageResolver { + + @Override + public String content() { + return this.message.text(); + } + + @Override + public @NotNull CompletableFuture resolveSignedMessage(final String argumentName, final CommandContext erased) throws CommandSyntaxException { + final CommandContext type = erased; + final CompletableFuture future = new CompletableFuture<>(); + + final MessageArgument.Message response = type.getArgument(argumentName, SignedMessageResolverImpl.class).message; + MessageArgument.resolveChatMessage(response, type, 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..5f0b46cb2b3f53a81a949fa64690133baa0a304b --- /dev/null +++ b/src/main/java/io/papermc/paper/command/brigadier/argument/VanillaArgumentProviderImpl.java @@ -0,0 +1,321 @@ +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.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.math.Position; +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.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.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 org.bukkit.GameMode; +import org.bukkit.HeightMap; +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 entity() { + return this.wrap(EntityArgument.entity(), (result) -> sourceStack -> { + return List.of(result.findSingleEntity((CommandSourceStack) sourceStack).getBukkitEntity()); + }); + } + + @Override + public ArgumentType entities() { + return this.wrap(EntityArgument.entities(), (result) -> sourceStack -> { + return Lists.transform(result.findEntities((CommandSourceStack) sourceStack), net.minecraft.world.entity.Entity::getBukkitEntity); + }); + } + + @Override + public ArgumentType player() { + return this.wrap(EntityArgument.player(), (result) -> sourceStack -> { + return List.of(result.findSinglePlayer((CommandSourceStack) sourceStack).getBukkitEntity()); + }); + } + + @Override + public ArgumentType players() { + return this.wrap(EntityArgument.players(), (result) -> sourceStack -> { + return Lists.transform(result.findPlayers((CommandSourceStack) sourceStack), ServerPlayer::getBukkitEntity); + }); + } + + @Override + public ArgumentType 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 blockPosition() { + return this.wrap(BlockPosArgument.blockPos(), (result) -> sourceStack -> { + final BlockPos pos = result.getBlockPos((CommandSourceStack) sourceStack); + + return Position.block(pos.getX(), pos.getY(), pos.getZ()); + }); + } + + @Override + public ArgumentType 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() { + return this.wrap(ItemArgument.item(PaperCommands.INSTANCE.getBuildContext()), (result) -> { + return CraftItemStack.asBukkitCopy(result.createItemStack(1, true)); + }); + } + + @Override + public ArgumentType itemStackPredicate() { + return this.wrap(ItemPredicateArgument.itemPredicate(PaperCommands.INSTANCE.getBuildContext()), type -> itemStack -> type.test(CraftItemStack.asNMSCopy(itemStack))); + } + + @Override + public ArgumentType 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() { + return this.wrap(ComponentArgument.textComponent(PaperCommands.INSTANCE.getBuildContext()), PaperAdventure::asAdventure); + } + + @Override + public ArgumentType