mirror of
https://github.com/PaperMC/Paper.git
synced 2025-01-26 10:11:32 +01:00
ea24e2c6aa
Fixes #11649 - As noted in the issue, when CommandNodes are serialized they are used as the key in a Map. Their equals()/hashcode() should only match if they are equal nodes (name & command), but due to the erasure of the command field pre-serialization, nodes with different commands can be mapped onto the same value. This causes the client to interpret both nodes as the same, causing suggestions where they should not. This is fixed by creating a different no-op command for the erasure, instead of them holding the same lambda.
373 lines
22 KiB
Diff
373 lines
22 KiB
Diff
--- a/net/minecraft/commands/Commands.java
|
|
+++ b/net/minecraft/commands/Commands.java
|
|
@@ -138,6 +138,14 @@
|
|
import net.minecraft.world.flag.FeatureFlags;
|
|
import net.minecraft.world.level.GameRules;
|
|
import org.slf4j.Logger;
|
|
+
|
|
+// CraftBukkit start
|
|
+import com.google.common.base.Joiner;
|
|
+import java.util.Collection;
|
|
+import java.util.LinkedHashSet;
|
|
+import org.bukkit.event.player.PlayerCommandSendEvent;
|
|
+import org.bukkit.event.server.ServerCommandEvent;
|
|
+// CraftBukkit end
|
|
|
|
public class Commands {
|
|
|
|
@@ -151,6 +159,7 @@
|
|
private final com.mojang.brigadier.CommandDispatcher<CommandSourceStack> dispatcher = new com.mojang.brigadier.CommandDispatcher();
|
|
|
|
public Commands(Commands.CommandSelection environment, CommandBuildContext commandRegistryAccess) {
|
|
+ // Paper
|
|
AdvancementCommands.register(this.dispatcher);
|
|
AttributeCommand.register(this.dispatcher, commandRegistryAccess);
|
|
ExecuteCommand.register(this.dispatcher, commandRegistryAccess);
|
|
@@ -250,8 +259,33 @@
|
|
|
|
if (environment.includeIntegrated) {
|
|
PublishCommand.register(this.dispatcher);
|
|
+ }
|
|
+
|
|
+ // Paper start - Vanilla command permission fixes
|
|
+ for (final CommandNode<CommandSourceStack> node : this.dispatcher.getRoot().getChildren()) {
|
|
+ if (node.getRequirement() == com.mojang.brigadier.builder.ArgumentBuilder.<CommandSourceStack>defaultRequirement()) {
|
|
+ node.requirement = stack -> stack.source == CommandSource.NULL || stack.getBukkitSender().hasPermission(org.bukkit.craftbukkit.command.VanillaCommandWrapper.getPermission(node));
|
|
+ }
|
|
}
|
|
+ // Paper end - Vanilla command permission fixes
|
|
+ // 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());
|
|
}
|
|
|
|
@@ -262,30 +296,72 @@
|
|
return new ParseResults(commandcontextbuilder1, parseResults.getReader(), parseResults.getExceptions());
|
|
}
|
|
|
|
+ // CraftBukkit start
|
|
+ public void dispatchServerCommand(CommandSourceStack sender, String command) {
|
|
+ Joiner joiner = Joiner.on(" ");
|
|
+ if (command.startsWith("/")) {
|
|
+ command = command.substring(1);
|
|
+ }
|
|
+
|
|
+ ServerCommandEvent event = new ServerCommandEvent(sender.getBukkitSender(), command);
|
|
+ org.bukkit.Bukkit.getPluginManager().callEvent(event);
|
|
+ if (event.isCancelled()) {
|
|
+ return;
|
|
+ }
|
|
+ command = event.getCommand();
|
|
+
|
|
+ String[] args = command.split(" ");
|
|
+ if (args.length == 0) return; // Paper - empty commands shall not be dispatched
|
|
+
|
|
+ // Paper - Fix permission levels for command blocks
|
|
+
|
|
+ // Handle vanilla commands; // Paper - handled in CommandNode/CommandDispatcher
|
|
+
|
|
+ String newCommand = joiner.join(args);
|
|
+ this.performPrefixedCommand(sender, newCommand, newCommand);
|
|
+ }
|
|
+ // CraftBukkit end
|
|
+
|
|
public void performPrefixedCommand(CommandSourceStack source, String command) {
|
|
- command = command.startsWith("/") ? command.substring(1) : command;
|
|
- this.performCommand(this.dispatcher.parse(command, source), command);
|
|
+ // CraftBukkit start
|
|
+ this.performPrefixedCommand(source, command, command);
|
|
+ }
|
|
+
|
|
+ public void performPrefixedCommand(CommandSourceStack commandlistenerwrapper, String s, String label) {
|
|
+ s = s.startsWith("/") ? s.substring(1) : s;
|
|
+ this.performCommand(this.dispatcher.parse(s, commandlistenerwrapper), s, label);
|
|
+ // CraftBukkit end
|
|
}
|
|
|
|
public void performCommand(ParseResults<CommandSourceStack> parseResults, String command) {
|
|
- CommandSourceStack commandlistenerwrapper = (CommandSourceStack) parseResults.getContext().getSource();
|
|
+ this.performCommand(parseResults, command, command);
|
|
+ }
|
|
|
|
+ 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(() -> {
|
|
- return "/" + command;
|
|
+ return "/" + s;
|
|
});
|
|
- ContextChain<CommandSourceStack> contextchain = Commands.finishParsing(parseResults, command, commandlistenerwrapper);
|
|
+ ContextChain contextchain = this.finishParsing(parseresults, s, commandlistenerwrapper, label); // CraftBukkit // Paper - Add UnknownCommandEvent
|
|
|
|
try {
|
|
if (contextchain != null) {
|
|
Commands.executeCommandInContext(commandlistenerwrapper, (executioncontext) -> {
|
|
- ExecutionContext.queueInitialCommandExecution(executioncontext, command, contextchain, commandlistenerwrapper, CommandResultCallback.EMPTY);
|
|
+ ExecutionContext.queueInitialCommandExecution(executioncontext, s, contextchain, commandlistenerwrapper, CommandResultCallback.EMPTY);
|
|
});
|
|
}
|
|
} catch (Exception exception) {
|
|
+ if (throwCommandError) throw exception;
|
|
MutableComponent ichatmutablecomponent = Component.literal(exception.getMessage() == null ? exception.getClass().getName() : exception.getMessage());
|
|
|
|
- if (Commands.LOGGER.isDebugEnabled()) {
|
|
- Commands.LOGGER.error("Command exception: /{}", command, exception);
|
|
+ Commands.LOGGER.error("Command exception: /{}", s, exception); // Paper - always show execution exception in console log
|
|
+ if (commandlistenerwrapper.getServer().isDebugging() || Commands.LOGGER.isDebugEnabled()) { // Paper - Debugging
|
|
StackTraceElement[] astacktraceelement = exception.getStackTrace();
|
|
|
|
for (int i = 0; i < Math.min(astacktraceelement.length, 3); ++i) {
|
|
@@ -298,7 +374,7 @@
|
|
}));
|
|
if (SharedConstants.IS_RUNNING_IN_IDE) {
|
|
commandlistenerwrapper.sendFailure(Component.literal(Util.describeError(exception)));
|
|
- Commands.LOGGER.error("'/{}' threw an exception", command, exception);
|
|
+ Commands.LOGGER.error("'/{}' threw an exception", s, exception);
|
|
}
|
|
} finally {
|
|
Profiler.get().pop();
|
|
@@ -307,18 +383,22 @@
|
|
}
|
|
|
|
@Nullable
|
|
- private static ContextChain<CommandSourceStack> finishParsing(ParseResults<CommandSourceStack> parseResults, String command, CommandSourceStack source) {
|
|
+ private ContextChain<CommandSourceStack> finishParsing(ParseResults<CommandSourceStack> parseresults, String s, CommandSourceStack commandlistenerwrapper, String label) { // CraftBukkit // Paper - Add UnknownCommandEvent
|
|
try {
|
|
- Commands.validateParseResults(parseResults);
|
|
- return (ContextChain) ContextChain.tryFlatten(parseResults.getContext().build(command)).orElseThrow(() -> {
|
|
- return CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(parseResults.getReader());
|
|
+ Commands.validateParseResults(parseresults);
|
|
+ return (ContextChain) ContextChain.tryFlatten(parseresults.getContext().build(s)).orElseThrow(() -> {
|
|
+ return CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(parseresults.getReader());
|
|
});
|
|
} catch (CommandSyntaxException commandsyntaxexception) {
|
|
- source.sendFailure(ComponentUtils.fromMessage(commandsyntaxexception.getRawMessage()));
|
|
+ // Paper start - Add UnknownCommandEvent
|
|
+ final net.kyori.adventure.text.TextComponent.Builder builder = net.kyori.adventure.text.Component.text();
|
|
+ // commandlistenerwrapper.sendFailure(ComponentUtils.fromMessage(commandsyntaxexception.getRawMessage()));
|
|
+ builder.color(net.kyori.adventure.text.format.NamedTextColor.RED).append(io.papermc.paper.brigadier.PaperBrigadier.componentFromMessage(commandsyntaxexception.getRawMessage()));
|
|
+ // Paper end - Add UnknownCommandEvent
|
|
if (commandsyntaxexception.getInput() != null && commandsyntaxexception.getCursor() >= 0) {
|
|
int i = Math.min(commandsyntaxexception.getInput().length(), commandsyntaxexception.getCursor());
|
|
MutableComponent ichatmutablecomponent = Component.empty().withStyle(ChatFormatting.GRAY).withStyle((chatmodifier) -> {
|
|
- return chatmodifier.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + command));
|
|
+ return chatmodifier.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/" + label)); // CraftBukkit // Paper
|
|
});
|
|
|
|
if (i > 10) {
|
|
@@ -333,8 +413,18 @@
|
|
}
|
|
|
|
ichatmutablecomponent.append((Component) Component.translatable("command.context.here").withStyle(ChatFormatting.RED, ChatFormatting.ITALIC));
|
|
- source.sendFailure(ichatmutablecomponent);
|
|
+ // Paper start - Add UnknownCommandEvent
|
|
+ // commandlistenerwrapper.sendFailure(ichatmutablecomponent);
|
|
+ builder
|
|
+ .append(net.kyori.adventure.text.Component.newline())
|
|
+ .append(io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent));
|
|
}
|
|
+ org.bukkit.event.command.UnknownCommandEvent event = new org.bukkit.event.command.UnknownCommandEvent(commandlistenerwrapper.getBukkitSender(), s, org.spigotmc.SpigotConfig.unknownCommandMessage.isEmpty() ? null : builder.build());
|
|
+ org.bukkit.Bukkit.getServer().getPluginManager().callEvent(event);
|
|
+ if (event.message() != null) {
|
|
+ commandlistenerwrapper.sendFailure(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.message()), false);
|
|
+ // Paper end - Add UnknownCommandEvent
|
|
+ }
|
|
|
|
return null;
|
|
}
|
|
@@ -368,7 +458,7 @@
|
|
|
|
executioncontext1.close();
|
|
} finally {
|
|
- Commands.CURRENT_EXECUTION_CONTEXT.set((Object) null);
|
|
+ Commands.CURRENT_EXECUTION_CONTEXT.set(null); // CraftBukkit - decompile error
|
|
}
|
|
} else {
|
|
callback.accept(executioncontext);
|
|
@@ -377,30 +467,133 @@
|
|
}
|
|
|
|
public void sendCommands(ServerPlayer player) {
|
|
- Map<CommandNode<CommandSourceStack>, CommandNode<SharedSuggestionProvider>> map = Maps.newHashMap();
|
|
+ // Paper start - Send empty commands if tab completion is disabled
|
|
+ if (org.spigotmc.SpigotConfig.tabComplete < 0) {
|
|
+ player.connection.send(new ClientboundCommandsPacket(new RootCommandNode<>()));
|
|
+ return;
|
|
+ }
|
|
+ // Paper end - Send empty commands if tab completion is disabled
|
|
+ // CraftBukkit start
|
|
+ // Register Vanilla commands into builtRoot as before
|
|
+ // Paper start - Perf: Async command map building
|
|
+ // Copy root children to avoid concurrent modification during building
|
|
+ final Collection<CommandNode<CommandSourceStack>> commandNodes = new java.util.ArrayList<>(this.dispatcher.getRoot().getChildren());
|
|
+ COMMAND_SENDING_POOL.execute(() -> this.sendAsync(player, commandNodes));
|
|
+ }
|
|
+
|
|
+ // Fixed pool, but with discard policy
|
|
+ public static final java.util.concurrent.ExecutorService COMMAND_SENDING_POOL = new java.util.concurrent.ThreadPoolExecutor(
|
|
+ 2, 2, 0, java.util.concurrent.TimeUnit.MILLISECONDS,
|
|
+ new java.util.concurrent.LinkedBlockingQueue<>(),
|
|
+ new com.google.common.util.concurrent.ThreadFactoryBuilder()
|
|
+ .setNameFormat("Paper Async Command Builder Thread Pool - %1$d")
|
|
+ .setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER))
|
|
+ .build(),
|
|
+ new java.util.concurrent.ThreadPoolExecutor.DiscardPolicy()
|
|
+ );
|
|
+
|
|
+ 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
|
|
+ // Paper - brigadier API removes the need to fill the map twice
|
|
RootCommandNode<SharedSuggestionProvider> rootcommandnode = new RootCommandNode();
|
|
|
|
map.put(this.dispatcher.getRoot(), rootcommandnode);
|
|
- this.fillUsableCommands(this.dispatcher.getRoot(), rootcommandnode, player.createCommandSourceStack(), map);
|
|
+ this.fillUsableCommands(dispatcherRootChildren, rootcommandnode, player.createCommandSourceStack(), map); // Paper - Perf: Async command map building; pass copy of children
|
|
+
|
|
+ Collection<String> bukkit = new LinkedHashSet<>();
|
|
+ for (CommandNode node : rootcommandnode.getChildren()) {
|
|
+ bukkit.add(node.getName());
|
|
+ }
|
|
+ // Paper start - Perf: Async command map building
|
|
+ new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent<CommandSourceStack>(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, false).callEvent(); // Paper - Brigadier API
|
|
+ net.minecraft.server.MinecraftServer.getServer().execute(() -> {
|
|
+ runSync(player, bukkit, rootcommandnode);
|
|
+ });
|
|
+ }
|
|
+
|
|
+ private void runSync(ServerPlayer player, Collection<String> bukkit, RootCommandNode<SharedSuggestionProvider> rootcommandnode) {
|
|
+ // Paper end - Perf: Async command map building
|
|
+ new com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent<CommandSourceStack>(player.getBukkitEntity(), (RootCommandNode) rootcommandnode, true).callEvent(); // Paper - Brigadier API
|
|
+ PlayerCommandSendEvent event = new PlayerCommandSendEvent(player.getBukkitEntity(), new LinkedHashSet<>(bukkit));
|
|
+ event.getPlayer().getServer().getPluginManager().callEvent(event);
|
|
+
|
|
+ // Remove labels that were removed during the event
|
|
+ for (String orig : bukkit) {
|
|
+ if (!event.getCommands().contains(orig)) {
|
|
+ rootcommandnode.removeCommand(orig);
|
|
+ }
|
|
+ }
|
|
+ // CraftBukkit end
|
|
player.connection.send(new ClientboundCommandsPacket(rootcommandnode));
|
|
}
|
|
|
|
- private void fillUsableCommands(CommandNode<CommandSourceStack> tree, CommandNode<SharedSuggestionProvider> result, CommandSourceStack source, Map<CommandNode<CommandSourceStack>, CommandNode<SharedSuggestionProvider>> resultNodes) {
|
|
- Iterator iterator = tree.getChildren().iterator();
|
|
+ // 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
|
|
|
|
while (iterator.hasNext()) {
|
|
CommandNode<CommandSourceStack> commandnode2 = (CommandNode) iterator.next();
|
|
+ // Paper start - Brigadier API
|
|
+ if (commandnode2.clientNode != null) {
|
|
+ commandnode2 = commandnode2.clientNode;
|
|
+ }
|
|
+ // Paper end - Brigadier API
|
|
+ if ( !org.spigotmc.SpigotConfig.sendNamespaced && commandnode2.getName().contains( ":" ) ) continue; // Spigot
|
|
|
|
if (commandnode2.canUse(source)) {
|
|
- ArgumentBuilder<SharedSuggestionProvider, ?> argumentbuilder = commandnode2.createBuilder();
|
|
+ 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;
|
|
});
|
|
if (argumentbuilder.getCommand() != null) {
|
|
- argumentbuilder.executes((commandcontext) -> {
|
|
- return 0;
|
|
+ // Paper start - fix suggestions due to falsely equal nodes
|
|
+ argumentbuilder.executes(new com.mojang.brigadier.Command<io.papermc.paper.command.brigadier.CommandSourceStack>() {
|
|
+ @Override
|
|
+ public int run(com.mojang.brigadier.context.CommandContext<io.papermc.paper.command.brigadier.CommandSourceStack> commandContext) throws CommandSyntaxException {
|
|
+ return 0;
|
|
+ }
|
|
});
|
|
+ // Paper end
|
|
}
|
|
|
|
if (argumentbuilder instanceof RequiredArgumentBuilder) {
|
|
@@ -415,12 +608,12 @@
|
|
argumentbuilder.redirect((CommandNode) resultNodes.get(argumentbuilder.getRedirect()));
|
|
}
|
|
|
|
- CommandNode<SharedSuggestionProvider> commandnode3 = argumentbuilder.build();
|
|
+ CommandNode commandnode3 = argumentbuilder.build(); // CraftBukkit - decompile error
|
|
|
|
resultNodes.put(commandnode2, commandnode3);
|
|
result.addChild(commandnode3);
|
|
if (!commandnode2.getChildren().isEmpty()) {
|
|
- this.fillUsableCommands(commandnode2, commandnode3, source, resultNodes);
|
|
+ this.fillUsableCommands(commandnode2.getChildren(), commandnode3, source, resultNodes); // Paper - Perf: Async command map building; pass children directly
|
|
}
|
|
}
|
|
}
|
|
@@ -481,7 +674,7 @@
|
|
}
|
|
|
|
private <T> HolderLookup.RegistryLookup.Delegate<T> createLookup(final HolderLookup.RegistryLookup<T> original) {
|
|
- return new HolderLookup.RegistryLookup.Delegate<T>(this) {
|
|
+ return new HolderLookup.RegistryLookup.Delegate<T>() { // CraftBukkit - decompile error
|
|
@Override
|
|
public HolderLookup.RegistryLookup<T> parent() {
|
|
return original;
|