diff --git a/paper-server/patches/sources/com/mojang/brigadier/CommandDispatcher.java.patch b/paper-server/patches/sources/com/mojang/brigadier/CommandDispatcher.java.patch index dbb16b6bcb..acc12b7b56 100644 --- a/paper-server/patches/sources/com/mojang/brigadier/CommandDispatcher.java.patch +++ b/paper-server/patches/sources/com/mojang/brigadier/CommandDispatcher.java.patch @@ -32,6 +32,24 @@ continue; } +@@ -451,7 +459,7 @@ + } + + 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; + } + +@@ -465,7 +473,7 @@ + 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) { @@ -537,10 +545,14 @@ int i = 0; for (final CommandNode node : parent.getChildren()) { diff --git a/paper-server/patches/sources/com/mojang/brigadier/tree/CommandNode.java.patch b/paper-server/patches/sources/com/mojang/brigadier/tree/CommandNode.java.patch index a38dc4015a..da6f30a5d8 100644 --- a/paper-server/patches/sources/com/mojang/brigadier/tree/CommandNode.java.patch +++ b/paper-server/patches/sources/com/mojang/brigadier/tree/CommandNode.java.patch @@ -16,11 +16,13 @@ public abstract class CommandNode implements Comparable> { private final Map> children = new LinkedHashMap<>(); -@@ -32,6 +34,14 @@ +@@ -32,6 +34,16 @@ private final RedirectModifier modifier; 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); @@ -31,7 +33,7 @@ protected CommandNode(final Command command, final Predicate requirement, final CommandNode redirect, final RedirectModifier modifier, final boolean forks) { this.command = command; -@@ -61,7 +71,17 @@ +@@ -61,7 +73,17 @@ return this.modifier; } @@ -50,3 +52,15 @@ return this.requirement.test(source); } +@@ -183,4 +205,11 @@ + } + + 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/paper-server/patches/sources/net/minecraft/commands/CommandSource.java.patch b/paper-server/patches/sources/net/minecraft/commands/CommandSource.java.patch index dff2434aec..4d74948c36 100644 --- a/paper-server/patches/sources/net/minecraft/commands/CommandSource.java.patch +++ b/paper-server/patches/sources/net/minecraft/commands/CommandSource.java.patch @@ -8,7 +8,7 @@ + // 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/paper-server/patches/sources/net/minecraft/commands/CommandSourceStack.java.patch b/paper-server/patches/sources/net/minecraft/commands/CommandSourceStack.java.patch index 31a1359323..94ee38720d 100644 --- a/paper-server/patches/sources/net/minecraft/commands/CommandSourceStack.java.patch +++ b/paper-server/patches/sources/net/minecraft/commands/CommandSourceStack.java.patch @@ -1,17 +1,18 @@ --- a/net/minecraft/commands/CommandSourceStack.java +++ b/net/minecraft/commands/CommandSourceStack.java -@@ -45,8 +45,9 @@ +@@ -45,9 +45,9 @@ import net.minecraft.world.level.dimension.DimensionType; import net.minecraft.world.phys.Vec2; import net.minecraft.world.phys.Vec3; +import com.mojang.brigadier.tree.CommandNode; // CraftBukkit -public class CommandSourceStack implements ExecutionCommandSource, SharedSuggestionProvider { -+public class CommandSourceStack implements ExecutionCommandSource, SharedSuggestionProvider, com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource { // Paper - Brigadier API - +- ++public class CommandSourceStack implements ExecutionCommandSource, 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")); -@@ -65,6 +66,8 @@ + public final CommandSource source; +@@ -65,6 +65,8 @@ private final Vec2 rotation; private final CommandSigningContext signingContext; private final TaskChainer chatMessageChainer; @@ -20,31 +21,9 @@ public CommandSourceStack(CommandSource output, Vec3 pos, Vec2 rot, ServerLevel world, int level, String name, Component displayName, MinecraftServer server, @Nullable Entity entity) { this(output, pos, rot, world, level, name, displayName, server, entity, false, CommandResultCallback.EMPTY, EntityAnchorArgument.Anchor.FEET, CommandSigningContext.ANONYMOUS, TaskChainer.immediate(server)); -@@ -169,11 +172,66 @@ - return this.textName; - } +@@ -171,8 +173,43 @@ -+ // 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 + // Paper start - Thread Safe Vanilla Command permission checking @@ -56,8 +35,8 @@ + // CraftBukkit end + return this.permissionLevel >= level; - } - ++ } ++ + // Paper start - Fix permission levels for command blocks + private boolean forceRespectPermissionLevel() { + return this.source == CommandSource.NULL || (this.source instanceof final net.minecraft.world.level.BaseCommandBlock commandBlock && commandBlock.getLevel().paperConfig().commandBlocks.forceFollowPermLevel); @@ -81,13 +60,12 @@ + } + return hasBukkitPerm.getAsBoolean(); + // Paper end - Fix permission levels for command blocks -+ } + } + // CraftBukkit end -+ + public Vec3 getPosition() { return this.worldPosition; - } -@@ -302,21 +360,26 @@ +@@ -302,21 +339,26 @@ while (iterator.hasNext()) { ServerPlayer entityplayer = (ServerPlayer) iterator.next(); @@ -117,11 +95,17 @@ } } -@@ -400,4 +463,10 @@ +@@ -400,4 +442,16 @@ public boolean isSilent() { 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/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch b/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch index a21b89d8de..48fe9bfe7f 100644 --- a/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch +++ b/paper-server/patches/sources/net/minecraft/commands/Commands.java.patch @@ -19,30 +19,45 @@ private final com.mojang.brigadier.CommandDispatcher 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); -@@ -252,6 +261,18 @@ - PublishCommand.register(this.dispatcher); - } +@@ -250,8 +259,33 @@ + if (environment.includeIntegrated) { + PublishCommand.register(this.dispatcher); ++ } ++ + // Paper start - Vanilla command permission fixes + for (final CommandNode node : this.dispatcher.getRoot().getChildren()) { + if (node.getRequirement() == com.mojang.brigadier.builder.ArgumentBuilder.defaultRequirement()) { + node.requirement = stack -> stack.source == CommandSource.NULL || stack.getBukkitSender().hasPermission(org.bukkit.craftbukkit.command.VanillaCommandWrapper.getPermission(node)); + } -+ } + } + // 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 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 flattenedAliasTarget = node; ++ while (flattenedAliasTarget.getRedirect() != null) flattenedAliasTarget = flattenedAliasTarget.getRedirect(); + ++ this.dispatcher.register( ++ com.mojang.brigadier.builder.LiteralArgumentBuilder.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 +283,69 @@ +@@ -262,30 +296,75 @@ return new ParseResults(commandcontextbuilder1, parseResults.getReader(), parseResults.getExceptions()); } @@ -75,14 +90,14 @@ + + String newCommand = joiner.join(args); + this.performPrefixedCommand(sender, newCommand, newCommand); - } ++ } + // CraftBukkit end - ++ + public void performPrefixedCommand(CommandSourceStack source, String 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); @@ -95,6 +110,11 @@ + } + public void performCommand(ParseResults parseresults, String s, String label) { // CraftBukkit ++ // Paper start ++ this.performCommand(parseresults, s, label, false); ++ } ++ public void performCommand(ParseResults parseresults, String s, String label, boolean throwCommandError) { ++ // Paper end + CommandSourceStack commandlistenerwrapper = (CommandSourceStack) parseresults.getContext().getSource(); + Profiler.get().push(() -> { @@ -112,16 +132,17 @@ }); } } 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 -+ Commands.LOGGER.error("Command exception: /{}", s, exception); StackTraceElement[] astacktraceelement = exception.getStackTrace(); for (int i = 0; i < Math.min(astacktraceelement.length, 3); ++i) { -@@ -298,7 +358,7 @@ +@@ -298,7 +377,7 @@ })); if (SharedConstants.IS_RUNNING_IN_IDE) { commandlistenerwrapper.sendFailure(Component.literal(Util.describeError(exception))); @@ -130,7 +151,7 @@ } } finally { Profiler.get().pop(); -@@ -307,18 +367,22 @@ +@@ -307,18 +386,22 @@ } @Nullable @@ -159,7 +180,7 @@ }); if (i > 10) { -@@ -333,8 +397,18 @@ +@@ -333,8 +416,18 @@ } ichatmutablecomponent.append((Component) Component.translatable("command.context.here").withStyle(ChatFormatting.RED, ChatFormatting.ITALIC)); @@ -179,7 +200,7 @@ return null; } -@@ -368,7 +442,7 @@ +@@ -368,7 +461,7 @@ executioncontext1.close(); } finally { @@ -188,7 +209,7 @@ } } else { callback.accept(executioncontext); -@@ -377,22 +451,89 @@ +@@ -377,23 +470,121 @@ } public void sendCommands(ServerPlayer player) { @@ -221,13 +242,7 @@ + private void sendAsync(ServerPlayer player, Collection> dispatcherRootChildren) { + // Paper end - Perf: Async command map building + Map, CommandNode> map = Maps.newIdentityHashMap(); // Use identity to prevent aliasing issues -+ RootCommandNode vanillaRoot = new RootCommandNode(); -+ -+ RootCommandNode 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 rootcommandnode = new RootCommandNode(); map.put(this.dispatcher.getRoot(), rootcommandnode); @@ -265,6 +280,7 @@ - Iterator iterator = tree.getChildren().iterator(); + // Paper start - Perf: Async command map building; pass copy of children + private void fillUsableCommands(Collection> children, CommandNode result, CommandSourceStack source, Map, CommandNode> 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 @@ -280,10 +296,47 @@ if (commandnode2.canUse(source)) { - ArgumentBuilder 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 redirect = argumentbuilder.getRedirect(); ++ // Diff copied from LiteralCommand#createBuilder ++ final com.mojang.brigadier.builder.LiteralArgumentBuilder 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 child : redirect.getChildren()) { ++ builder.then(child); ++ } ++ ++ argumentbuilder = builder; ++ } ++ // Paper end ++ argumentbuilder.requires((icompletionprovider) -> { return true; -@@ -415,12 +556,12 @@ + }); +@@ -415,12 +606,12 @@ argumentbuilder.redirect((CommandNode) resultNodes.get(argumentbuilder.getRedirect())); } @@ -298,7 +351,7 @@ } } } -@@ -481,7 +622,7 @@ +@@ -481,7 +672,7 @@ } private HolderLookup.RegistryLookup.Delegate createLookup(final HolderLookup.RegistryLookup original) { diff --git a/paper-server/patches/sources/net/minecraft/commands/arguments/MessageArgument.java.patch b/paper-server/patches/sources/net/minecraft/commands/arguments/MessageArgument.java.patch index 647293ea7e..ba048d7f68 100644 --- a/paper-server/patches/sources/net/minecraft/commands/arguments/MessageArgument.java.patch +++ b/paper-server/patches/sources/net/minecraft/commands/arguments/MessageArgument.java.patch @@ -1,6 +1,18 @@ --- a/net/minecraft/commands/arguments/MessageArgument.java +++ b/net/minecraft/commands/arguments/MessageArgument.java -@@ -54,17 +54,21 @@ +@@ -40,6 +40,11 @@ + + public static void resolveChatMessage(CommandContext context, String name, Consumer 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 context, String name, Consumer callback) throws CommandSyntaxException { ++ // Paper end + CommandSourceStack commandSourceStack = context.getSource(); + Component component = message.resolveComponent(commandSourceStack); + CommandSigningContext commandSigningContext = commandSourceStack.getSigningContext(); +@@ -54,17 +59,21 @@ private static void resolveSignedMessage(Consumer callback, CommandSourceStack source, PlayerChatMessage message) { MinecraftServer minecraftServer = source.getServer(); CompletableFuture completableFuture = filterPlainText(source, message); diff --git a/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch index 3c886f0c9e..7694b58d27 100644 --- a/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/MinecraftServer.java.patch @@ -162,7 +162,7 @@ + public static int currentTick; // Paper - improve tick loop + public java.util.Queue processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); + public int autosavePeriod; -+ public Commands vanillaCommandDispatcher; ++ // Paper - don't store the vanilla dispatcher + private boolean forceTicks; + // CraftBukkit end + // Spigot start @@ -202,7 +202,7 @@ this.metricsRecorder = InactiveMetricsRecorder.INSTANCE; this.onMetricsRecordingStopped = (methodprofilerresults) -> { this.stopRecordingMetrics(); -@@ -319,36 +373,68 @@ +@@ -319,36 +373,67 @@ this.scoreboard = new ServerScoreboard(this); this.customBossEvents = new CustomBossEvents(); this.suppressedExceptions = new SuppressedExceptionCollector(); @@ -254,7 +254,6 @@ + // 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 + /* @@ -286,7 +285,7 @@ } private void readScoreboard(DimensionDataStorage persistentStateManager) { -@@ -357,7 +443,7 @@ +@@ -357,7 +442,7 @@ protected abstract boolean initServer() throws IOException; @@ -295,7 +294,7 @@ if (!JvmProfiler.INSTANCE.isRunning()) { ; } -@@ -365,12 +451,8 @@ +@@ -365,12 +450,8 @@ boolean flag = false; ProfiledDuration profiledduration = JvmProfiler.INSTANCE.onWorldLoadedStarted(); @@ -309,7 +308,7 @@ if (profiledduration != null) { profiledduration.finish(true); } -@@ -387,23 +469,241 @@ +@@ -387,23 +468,244 @@ protected void forceDifficulty() {} @@ -549,6 +548,9 @@ + + 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(); + } @@ -565,7 +567,7 @@ if (!iworlddataserver.isInitialized()) { try { -@@ -427,30 +727,8 @@ +@@ -427,30 +729,8 @@ iworlddataserver.setInitialized(true); } @@ -597,7 +599,7 @@ private static void setInitialSpawn(ServerLevel world, ServerLevelData worldProperties, boolean bonusChest, boolean debugWorld) { if (debugWorld) { -@@ -458,6 +736,21 @@ +@@ -458,6 +738,21 @@ } else { ServerChunkCache chunkproviderserver = world.getChunkSource(); ChunkPos chunkcoordintpair = new ChunkPos(chunkproviderserver.randomState().sampler().findSpawnPosition()); @@ -619,7 +621,7 @@ int i = chunkproviderserver.getGenerator().getSpawnHeight(world); if (i < world.getMinY()) { -@@ -516,31 +809,36 @@ +@@ -516,31 +811,36 @@ iworlddataserver.setGameType(GameType.SPECTATOR); } @@ -667,7 +669,7 @@ ForcedChunksSavedData forcedchunk = (ForcedChunksSavedData) worldserver1.getDataStorage().get(ForcedChunksSavedData.factory(), "chunks"); if (forcedchunk != null) { -@@ -555,10 +853,17 @@ +@@ -555,10 +855,17 @@ } } @@ -689,7 +691,7 @@ } public GameType getDefaultGameType() { -@@ -588,12 +893,16 @@ +@@ -588,12 +895,16 @@ worldserver.save((ProgressListener) null, flush, worldserver.noSave && !force); } @@ -708,7 +710,7 @@ if (flush) { Iterator iterator1 = this.getAllLevels().iterator(); -@@ -628,18 +937,45 @@ +@@ -628,18 +939,45 @@ this.stopServer(); } @@ -755,7 +757,7 @@ } MinecraftServer.LOGGER.info("Saving worlds"); -@@ -693,6 +1029,15 @@ +@@ -693,6 +1031,15 @@ } catch (IOException ioexception1) { MinecraftServer.LOGGER.error("Failed to unlock level {}", this.storageSource.getLevelId(), ioexception1); } @@ -771,7 +773,7 @@ } -@@ -709,6 +1054,14 @@ +@@ -709,6 +1056,14 @@ } public void halt(boolean waitForShutdown) { @@ -786,7 +788,7 @@ this.running = false; if (waitForShutdown) { try { -@@ -720,6 +1073,64 @@ +@@ -720,6 +1075,64 @@ } @@ -851,14 +853,14 @@ protected void runServer() { try { if (!this.initServer()) { -@@ -727,9 +1138,26 @@ +@@ -727,8 +1140,25 @@ } this.nextTickTimeNanos = Util.getNanos(); - this.statusIcon = (ServerStatus.Favicon) this.loadStatusIcon().orElse((Object) null); + this.statusIcon = (ServerStatus.Favicon) this.loadStatusIcon().orElse(null); // CraftBukkit - decompile error this.status = this.buildServerStatus(); - ++ + // Spigot start + org.spigotmc.WatchdogThread.hasStarted = true; // Paper + Arrays.fill( this.recentTps, 20 ); @@ -875,11 +877,10 @@ + LOGGER.info("*************************************************************************************"); + } + // Paper end - Add onboarding message for initial server start -+ + while (this.running) { long i; - -@@ -744,12 +1172,31 @@ +@@ -744,11 +1174,30 @@ if (j > MinecraftServer.OVERLOADED_THRESHOLD_NANOS + 20L * i && this.nextTickTimeNanos - this.lastOverloadWarningNanos >= MinecraftServer.OVERLOADED_WARNING_INTERVAL_NANOS + 100L * i) { long k = j / i; @@ -888,7 +889,7 @@ this.nextTickTimeNanos += k * i; this.lastOverloadWarningNanos = this.nextTickTimeNanos; } - } ++ } + // Spigot start + // Paper start - further improve server tick loop + currentTime = Util.getNanos(); @@ -898,20 +899,19 @@ + tps1.add(currentTps, diff); + tps5.add(currentTps, diff); + tps15.add(currentTps, diff); - ++ + // Backwards compat with bad plugins + this.recentTps[0] = tps1.getAverage(); + this.recentTps[1] = tps5.getAverage(); + this.recentTps[2] = tps15.getAverage(); + tickSection = currentTime; -+ } + } + // Paper end - further improve server tick loop + // Spigot end -+ + boolean flag = i == 0L; - if (this.debugCommandProfilerDelayStart) { -@@ -757,6 +1204,8 @@ +@@ -757,6 +1206,8 @@ this.debugCommandProfiler = new MinecraftServer.TimeProfiler(Util.getNanos(), this.tickCount); } @@ -920,7 +920,7 @@ this.nextTickTimeNanos += i; try { -@@ -830,6 +1279,14 @@ +@@ -830,6 +1281,14 @@ this.services.profileCache().clearExecutor(); } @@ -935,7 +935,7 @@ this.onServerExit(); } -@@ -889,9 +1346,16 @@ +@@ -889,9 +1348,16 @@ } private boolean haveTime() { @@ -953,7 +953,7 @@ public static boolean throwIfFatalException() { RuntimeException runtimeexception = (RuntimeException) MinecraftServer.fatalException.get(); -@@ -903,7 +1367,7 @@ +@@ -903,7 +1369,7 @@ } public static void setFatalException(RuntimeException exception) { @@ -962,7 +962,7 @@ } @Override -@@ -961,6 +1425,7 @@ +@@ -961,6 +1427,7 @@ if (super.pollTask()) { return true; } else { @@ -970,7 +970,7 @@ if (this.tickRateManager.isSprinting() || this.haveTime()) { Iterator iterator = this.getAllLevels().iterator(); -@@ -968,16 +1433,16 @@ +@@ -968,16 +1435,16 @@ ServerLevel worldserver = (ServerLevel) iterator.next(); if (worldserver.getChunkSource().pollTask()) { @@ -990,7 +990,7 @@ Profiler.get().incrementCounter("runTask"); super.doRunTask(ticktask); } -@@ -1025,6 +1490,7 @@ +@@ -1025,6 +1492,7 @@ } public void tickServer(BooleanSupplier shouldKeepTicking) { @@ -998,7 +998,7 @@ long i = Util.getNanos(); int j = this.pauseWhileEmptySeconds() * 20; -@@ -1041,11 +1507,13 @@ +@@ -1041,11 +1509,13 @@ this.autoSave(); } @@ -1012,7 +1012,7 @@ ++this.tickCount; this.tickRateManager.tick(); this.tickChildren(shouldKeepTicking); -@@ -1055,12 +1523,18 @@ +@@ -1055,12 +1525,18 @@ } --this.ticksUntilAutosave; @@ -1032,7 +1032,7 @@ gameprofilerfiller.push("tallying"); long k = Util.getNanos() - i; int l = this.tickCount % 100; -@@ -1069,12 +1543,17 @@ +@@ -1069,12 +1545,17 @@ this.aggregatedTickTimesNanos += k; this.tickTimesNanos[l] = k; this.smoothedTickTimeMillis = this.smoothedTickTimeMillis * 0.8F + (float) k / (float) TimeUtil.NANOSECONDS_PER_MILLISECOND * 0.19999999F; @@ -1051,7 +1051,7 @@ MinecraftServer.LOGGER.debug("Autosave started"); ProfilerFiller gameprofilerfiller = Profiler.get(); -@@ -1123,7 +1602,7 @@ +@@ -1123,7 +1604,7 @@ private ServerStatus buildServerStatus() { ServerStatus.Players serverping_serverpingplayersample = this.buildPlayerStatus(); @@ -1060,7 +1060,7 @@ } private ServerStatus.Players buildPlayerStatus() { -@@ -1133,7 +1612,7 @@ +@@ -1133,7 +1614,7 @@ if (this.hidesOnlinePlayers()) { return new ServerStatus.Players(i, list.size(), List.of()); } else { @@ -1069,7 +1069,7 @@ ObjectArrayList objectarraylist = new ObjectArrayList(j); int k = Mth.nextInt(this.random, 0, list.size() - j); -@@ -1154,24 +1633,72 @@ +@@ -1154,24 +1635,72 @@ this.getPlayerList().getPlayers().forEach((entityplayer) -> { entityplayer.connection.suspendFlushing(); }); @@ -1100,7 +1100,7 @@ + while (!this.processQueue.isEmpty()) { + this.processQueue.remove().run(); + } - ++ + // Send time updates to everyone, it will get the right time from the world the player is in. + // Paper start - Perf: Optimize time updates + for (final ServerLevel level : this.getAllLevels()) { @@ -1120,7 +1120,7 @@ + // Paper end - Perf: Optimize time updates + } + } -+ + + this.isIteratingOverLevels = true; // Paper - Throw exception on world create while being ticked + Iterator iterator = this.getAllLevels().iterator(); // Paper - Throw exception on world create while being ticked; move down while (iterator.hasNext()) { @@ -1143,7 +1143,7 @@ gameprofilerfiller.push("tick"); -@@ -1186,7 +1713,9 @@ +@@ -1186,7 +1715,9 @@ gameprofilerfiller.pop(); gameprofilerfiller.pop(); @@ -1153,7 +1153,7 @@ gameprofilerfiller.popPush("connection"); this.tickConnection(); -@@ -1267,6 +1796,22 @@ +@@ -1267,6 +1798,22 @@ return (ServerLevel) this.levels.get(key); } @@ -1176,7 +1176,7 @@ public Set> levelKeys() { return this.levels.keySet(); } -@@ -1296,7 +1841,7 @@ +@@ -1296,7 +1843,7 @@ @DontObfuscate public String getServerModName() { @@ -1185,7 +1185,7 @@ } public SystemReport fillSystemReport(SystemReport details) { -@@ -1347,7 +1892,7 @@ +@@ -1347,7 +1894,7 @@ @Override public void sendSystemMessage(Component message) { @@ -1194,7 +1194,7 @@ } public KeyPair getKeyPair() { -@@ -1385,11 +1930,14 @@ +@@ -1385,11 +1932,14 @@ } } @@ -1214,7 +1214,7 @@ } } -@@ -1403,7 +1951,7 @@ +@@ -1403,7 +1953,7 @@ while (iterator.hasNext()) { ServerLevel worldserver = (ServerLevel) iterator.next(); @@ -1223,7 +1223,7 @@ } } -@@ -1481,10 +2029,20 @@ +@@ -1481,10 +2031,20 @@ @Override public String getMotd() { @@ -1245,7 +1245,7 @@ this.motd = motd; } -@@ -1507,7 +2065,7 @@ +@@ -1507,7 +2067,7 @@ } public ServerConnectionListener getConnection() { @@ -1254,7 +1254,7 @@ } public boolean isReady() { -@@ -1593,7 +2151,7 @@ +@@ -1593,7 +2153,7 @@ @Override public void executeIfPossible(Runnable runnable) { if (this.isStopped()) { @@ -1263,7 +1263,7 @@ } else { super.executeIfPossible(runnable); } -@@ -1632,13 +2190,19 @@ +@@ -1632,13 +2192,19 @@ return this.functionManager; } @@ -1285,14 +1285,15 @@ }, this).thenCompose((immutablelist) -> { MultiPackResourceManager resourcemanager = new MultiPackResourceManager(PackType.SERVER_DATA, immutablelist); List> list = TagLoader.loadTagsForExistingRegistries(resourcemanager, this.registries.compositeAccess()); -@@ -1654,17 +2218,21 @@ +@@ -1652,6 +2218,7 @@ + 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()); - +@@ -1660,11 +2227,23 @@ this.worldData.setDataConfiguration(worlddataconfiguration); this.resources.managers.updateStaticRegistryTags(); this.resources.managers.getRecipeManager().finalizeRecipeLoading(this.worldData.enabledFeatures()); @@ -1303,11 +1304,20 @@ 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); if (this.isSameThread()) { -@@ -1789,14 +2357,15 @@ +@@ -1789,14 +2368,15 @@ if (this.isEnforceWhitelist()) { PlayerList playerlist = source.getServer().getPlayerList(); UserWhiteList whitelist = playerlist.getWhiteList(); @@ -1325,7 +1335,7 @@ } } -@@ -1952,7 +2521,7 @@ +@@ -1952,7 +2532,7 @@ final List list = Lists.newArrayList(); final GameRules gamerules = this.getGameRules(); @@ -1334,7 +1344,7 @@ @Override public > void visit(GameRules.Key key, GameRules.Type type) { list.add(String.format(Locale.ROOT, "%s=%s\n", key.getId(), gamerules.getRule(key))); -@@ -2058,7 +2627,7 @@ +@@ -2058,7 +2638,7 @@ try { label51: { @@ -1343,22 +1353,19 @@ try { arraylist = Lists.newArrayList(NativeModuleLister.listModules()); -@@ -2105,9 +2674,24 @@ - if (bufferedwriter != null) { - bufferedwriter.close(); - } -+ -+ } -+ +@@ -2108,6 +2688,21 @@ + + } + + // CraftBukkit start + public boolean isDebugging() { + return false; + } - ++ + public static MinecraftServer getServer() { + return SERVER; // Paper - } - ++ } ++ + @Deprecated + public static RegistryAccess getDefaultRegistryAccess() { + return CraftRegistry.getMinecraftRegistry(); @@ -1368,7 +1375,7 @@ private ProfilerFiller createProfiler() { if (this.willStartRecordingMetrics) { this.metricsRecorder = ActiveMetricsRecorder.createStarted(new ServerMetricsSamplersProvider(Util.timeSource, this.isDedicatedServer()), Util.timeSource, Util.ioPool(), new MetricsPersister("server"), this.onMetricsRecordingStopped, (path) -> { -@@ -2225,18 +2809,24 @@ +@@ -2225,18 +2820,24 @@ } public void logChatMessage(Component message, ChatType.Bound params, @Nullable String prefix) { @@ -1397,7 +1404,7 @@ } public boolean logIPs() { -@@ -2379,4 +2969,30 @@ +@@ -2379,4 +2980,30 @@ public static record ServerResourcePackInfo(UUID id, String url, String hash, boolean isRequired, @Nullable Component prompt) { } diff --git a/paper-server/patches/sources/net/minecraft/server/ReloadableServerResources.java.patch b/paper-server/patches/sources/net/minecraft/server/ReloadableServerResources.java.patch new file mode 100644 index 0000000000..59120184c0 --- /dev/null +++ b/paper-server/patches/sources/net/minecraft/server/ReloadableServerResources.java.patch @@ -0,0 +1,25 @@ +--- a/net/minecraft/server/ReloadableServerResources.java ++++ b/net/minecraft/server/ReloadableServerResources.java +@@ -39,6 +39,7 @@ + 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 @@ + 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/paper-server/patches/sources/net/minecraft/server/ServerFunctionManager.java.patch b/paper-server/patches/sources/net/minecraft/server/ServerFunctionManager.java.patch index c26f9aaa6c..45ee083997 100644 --- a/paper-server/patches/sources/net/minecraft/server/ServerFunctionManager.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/ServerFunctionManager.java.patch @@ -5,7 +5,7 @@ public CommandDispatcher getDispatcher() { - return this.server.getCommands().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/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch index ed862c303d..0d926bf2aa 100644 --- a/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/dedicated/DedicatedServer.java.patch @@ -163,7 +163,7 @@ DedicatedServer.LOGGER.info("Loading properties"); DedicatedServerProperties dedicatedserverproperties = this.settings.getProperties(); -@@ -126,14 +213,50 @@ +@@ -126,14 +213,49 @@ this.setPreventProxyConnections(dedicatedserverproperties.preventProxyConnections); this.setLocalIp(dedicatedserverproperties.serverIp); } @@ -188,7 +188,6 @@ + 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); @@ -215,7 +214,7 @@ InetAddress inetaddress = null; if (!this.getLocalIp().isEmpty()) { -@@ -143,34 +266,55 @@ +@@ -143,34 +265,55 @@ if (this.getPort() < 0) { this.setPort(dedicatedserverproperties.serverPort); } @@ -277,7 +276,7 @@ this.debugSampleSubscriptionTracker = new DebugSampleSubscriptionTracker(this.getPlayerList()); this.tickTimeLogger = new RemoteSampleLogger(TpsDebugDimensions.values().length, this.debugSampleSubscriptionTracker, RemoteDebugSampleType.TICK_TIME); long i = Util.getNanos(); -@@ -178,13 +322,13 @@ +@@ -178,13 +321,13 @@ SkullBlockEntity.setup(this.services, this); GameProfileCache.setUsesAuthentication(this.usesAuthentication()); DedicatedServer.LOGGER.info("Preparing level \"{}\"", this.getLevelIdName()); @@ -293,7 +292,7 @@ } if (dedicatedserverproperties.enableQuery) { -@@ -197,7 +341,7 @@ +@@ -197,7 +340,7 @@ this.rconThread = RconThread.create(this); } @@ -302,7 +301,7 @@ Thread thread1 = new Thread(new ServerWatchdog(this)); thread1.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandlerWithName(DedicatedServer.LOGGER)); -@@ -215,6 +359,12 @@ +@@ -215,6 +358,12 @@ } } @@ -315,7 +314,7 @@ @Override public boolean isSpawningMonsters() { return this.settings.getProperties().spawnMonsters && super.isSpawningMonsters(); -@@ -227,7 +377,7 @@ +@@ -227,7 +376,7 @@ @Override public void forceDifficulty() { @@ -324,7 +323,7 @@ } @Override -@@ -286,13 +436,14 @@ +@@ -286,13 +435,14 @@ } if (this.rconThread != null) { @@ -341,7 +340,7 @@ } @Override -@@ -302,19 +453,29 @@ +@@ -302,19 +452,29 @@ } @Override @@ -377,7 +376,7 @@ } } -@@ -383,7 +544,7 @@ +@@ -383,7 +543,7 @@ @Override public boolean isUnderSpawnProtection(ServerLevel world, BlockPos pos, Player player) { @@ -386,7 +385,7 @@ return false; } else if (this.getPlayerList().getOps().isEmpty()) { return false; -@@ -453,7 +614,11 @@ +@@ -453,7 +613,11 @@ public boolean enforceSecureProfile() { DedicatedServerProperties dedicatedserverproperties = this.getProperties(); @@ -399,7 +398,7 @@ } @Override -@@ -541,16 +706,52 @@ +@@ -541,16 +705,52 @@ @Override public String getPluginNames() { @@ -456,7 +455,7 @@ } public void storeUsingWhiteList(boolean useWhitelist) { -@@ -660,4 +861,15 @@ +@@ -660,4 +860,15 @@ } } } diff --git a/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch b/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch index 160c552296..33a7728608 100644 --- a/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch +++ b/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch @@ -1558,12 +1558,10 @@ } return optional; -@@ -1564,8 +2394,129 @@ - } - +@@ -1566,6 +2396,117 @@ return false; -+ } -+ + } + + // CraftBukkit start - add method + public void chat(String s, PlayerChatMessage original, boolean async) { + if (s.isEmpty() || this.player.getChatVisibility() == ChatVisiblity.HIDDEN) { @@ -1656,39 +1654,29 @@ + this.server.console.sendMessage(s); + } + } - } - -+ 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 + private PlayerChatMessage getSignedMessage(ServerboundChatPacket packet, LastSeenMessages lastSeenMessages) throws SignedMessageChain.DecodeException { SignedMessageBody signedmessagebody = new SignedMessageBody(packet.message(), packet.timeStamp(), packet.salt(), lastSeenMessages); -@@ -1573,15 +2524,44 @@ +@@ -1573,15 +2514,44 @@ } private void broadcastChatMessage(PlayerChatMessage message) { @@ -1739,7 +1727,7 @@ } -@@ -1592,7 +2572,7 @@ +@@ -1592,7 +2562,7 @@ synchronized (this.lastSeenMessages) { if (!this.lastSeenMessages.applyOffset(packet.offset())) { ServerGamePacketListenerImpl.LOGGER.warn("Failed to validate message acknowledgements from {}", this.player.getName().getString()); @@ -1748,7 +1736,7 @@ } } -@@ -1601,7 +2581,40 @@ +@@ -1601,7 +2571,40 @@ @Override public void handleAnimate(ServerboundSwingPacket packet) { PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); @@ -1789,7 +1777,7 @@ this.player.swing(packet.getHand()); } -@@ -1609,6 +2622,29 @@ +@@ -1609,6 +2612,29 @@ public void handlePlayerCommand(ServerboundPlayerCommandPacket packet) { PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); if (this.player.hasClientLoaded()) { @@ -1819,7 +1807,7 @@ this.player.resetLastActionTime(); Entity entity; PlayerRideableJumping ijumpable; -@@ -1616,6 +2652,11 @@ +@@ -1616,6 +2642,11 @@ switch (packet.getAction()) { case PRESS_SHIFT_KEY: this.player.setShiftKeyDown(true); @@ -1831,7 +1819,7 @@ break; case RELEASE_SHIFT_KEY: this.player.setShiftKeyDown(false); -@@ -1684,13 +2725,19 @@ +@@ -1684,13 +2715,19 @@ } if (i > 4096) { @@ -1852,7 +1840,7 @@ this.send(new ClientboundPlayerChatPacket(message.link().sender(), message.link().index(), message.signature(), message.signedBody().pack(this.messageSignatureCache), message.unsignedContent(), message.filterMask(), params)); this.addPendingMessage(message); } -@@ -1701,7 +2748,19 @@ +@@ -1701,7 +2738,19 @@ public SocketAddress getRemoteAddress() { return this.connection.getRemoteAddress(); @@ -1872,7 +1860,7 @@ public void switchToConfig() { this.waitingForSwitchToConfig = true; -@@ -1718,9 +2777,17 @@ +@@ -1718,9 +2767,17 @@ @Override public void handleInteract(ServerboundInteractPacket packet) { PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); @@ -1890,7 +1878,7 @@ this.player.resetLastActionTime(); this.player.setShiftKeyDown(packet.isUsingSecondaryAction()); -@@ -1733,20 +2800,59 @@ +@@ -1733,20 +2790,59 @@ if (this.player.canInteractWithEntity(axisalignedbb, 3.0D)) { packet.dispatch(new ServerboundInteractPacket.Handler() { @@ -1954,7 +1942,7 @@ } } -@@ -1755,19 +2861,20 @@ +@@ -1755,19 +2851,20 @@ @Override public void onInteraction(InteractionHand hand) { @@ -1978,7 +1966,7 @@ label23: { if (entity instanceof AbstractArrow) { -@@ -1785,17 +2892,41 @@ +@@ -1785,17 +2882,41 @@ } ServerGamePacketListenerImpl.this.player.attack(entity); @@ -2021,7 +2009,7 @@ } } -@@ -1809,7 +2940,7 @@ +@@ -1809,7 +2930,7 @@ case PERFORM_RESPAWN: if (this.player.wonGame) { this.player.wonGame = false; @@ -2030,7 +2018,7 @@ this.resetPosition(); CriteriaTriggers.CHANGED_DIMENSION.trigger(this.player, Level.END, Level.OVERWORLD); } else { -@@ -1817,11 +2948,11 @@ +@@ -1817,11 +2938,11 @@ return; } @@ -2045,7 +2033,7 @@ } } break; -@@ -1833,16 +2964,27 @@ +@@ -1833,16 +2954,27 @@ @Override public void handleContainerClose(ServerboundContainerClosePacket packet) { @@ -2075,7 +2063,7 @@ this.player.containerMenu.sendAllDataToRemote(); } else if (!this.player.containerMenu.stillValid(this.player)) { ServerGamePacketListenerImpl.LOGGER.debug("Player {} interacted with invalid menu {}", this.player, this.player.containerMenu); -@@ -1855,7 +2997,303 @@ +@@ -1855,7 +2987,303 @@ boolean flag = packet.getStateId() != this.player.containerMenu.getStateId(); this.player.containerMenu.suppressRemoteUpdates(); @@ -2380,7 +2368,7 @@ ObjectIterator objectiterator = Int2ObjectMaps.fastIterable(packet.getChangedSlots()).iterator(); while (objectiterator.hasNext()) { -@@ -1879,6 +3317,14 @@ +@@ -1879,6 +3307,14 @@ @Override public void handlePlaceRecipe(ServerboundPlaceRecipePacket packet) { @@ -2395,7 +2383,7 @@ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); this.player.resetLastActionTime(); if (!this.player.isSpectator() && this.player.containerMenu.containerId == packet.containerId()) { -@@ -1900,8 +3346,42 @@ +@@ -1900,8 +3336,42 @@ ServerGamePacketListenerImpl.LOGGER.debug("Player {} tried to place impossible recipe {}", this.player, recipeholder.id().location()); return; } @@ -2439,7 +2427,7 @@ if (containerrecipebook_a == RecipeBookMenu.PostPlaceAction.PLACE_GHOST_RECIPE) { this.player.connection.send(new ClientboundPlaceGhostRecipePacket(this.player.containerMenu.containerId, craftingmanager_d.display().display())); -@@ -1917,6 +3397,7 @@ +@@ -1917,6 +3387,7 @@ @Override public void handleContainerButtonClick(ServerboundContainerButtonClickPacket packet) { PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); @@ -2447,7 +2435,7 @@ this.player.resetLastActionTime(); if (this.player.containerMenu.containerId == packet.containerId() && !this.player.isSpectator()) { if (!this.player.containerMenu.stillValid(this.player)) { -@@ -1945,7 +3426,44 @@ +@@ -1945,7 +3416,44 @@ boolean flag1 = packet.slotNum() >= 1 && packet.slotNum() <= 45; boolean flag2 = itemstack.isEmpty() || itemstack.getCount() <= itemstack.getMaxStackSize(); @@ -2492,7 +2480,7 @@ if (flag1 && flag2) { this.player.inventoryMenu.getSlot(packet.slotNum()).setByPlayer(itemstack); this.player.inventoryMenu.setRemoteSlot(packet.slotNum(), itemstack); -@@ -1964,7 +3482,19 @@ +@@ -1964,7 +3472,19 @@ @Override public void handleSignUpdate(ServerboundSignUpdatePacket packet) { @@ -2513,7 +2501,7 @@ this.filterTextPacket(list).thenAcceptAsync((list1) -> { this.updateSignText(packet, list1); -@@ -1972,6 +3502,7 @@ +@@ -1972,6 +3492,7 @@ } private void updateSignText(ServerboundSignUpdatePacket packet, List signText) { @@ -2521,7 +2509,7 @@ this.player.resetLastActionTime(); ServerLevel worldserver = this.player.serverLevel(); BlockPos blockposition = packet.getPos(); -@@ -1993,15 +3524,33 @@ +@@ -1993,15 +3514,33 @@ @Override public void handlePlayerAbilities(ServerboundPlayerAbilitiesPacket packet) { PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); @@ -2556,7 +2544,7 @@ if (this.player.isModelPartShown(PlayerModelPart.HAT) != flag) { this.server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_HAT, this.player)); } -@@ -2012,7 +3561,7 @@ +@@ -2012,7 +3551,7 @@ public void handleChangeDifficulty(ServerboundChangeDifficultyPacket packet) { PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); if (this.player.hasPermissions(2) || this.isSingleplayerOwner()) { @@ -2565,7 +2553,7 @@ } } -@@ -2033,7 +3582,7 @@ +@@ -2033,7 +3572,7 @@ if (!Objects.equals(profilepublickey_a, profilepublickey_a1)) { if (profilepublickey_a != null && profilepublickey_a1.expiresAt().isBefore(profilepublickey_a.expiresAt())) { @@ -2574,7 +2562,7 @@ } else { try { SignatureValidator signaturevalidator = this.server.getProfileKeySignatureValidator(); -@@ -2045,8 +3594,8 @@ +@@ -2045,8 +3584,8 @@ this.resetPlayerChatState(remotechatsession_a.validate(this.player.getGameProfile(), signaturevalidator)); } catch (ProfilePublicKey.ValidationException profilepublickey_b) { @@ -2585,7 +2573,7 @@ } } -@@ -2058,7 +3607,7 @@ +@@ -2058,7 +3597,7 @@ if (!this.waitingForSwitchToConfig) { throw new IllegalStateException("Client acknowledged config, but none was requested"); } else { @@ -2594,7 +2582,7 @@ } } -@@ -2076,15 +3625,18 @@ +@@ -2076,15 +3615,18 @@ private void resetPlayerChatState(RemoteChatSession session) { this.chatSession = session; @@ -2616,7 +2604,7 @@ @Override public void handleClientTickEnd(ServerboundClientTickEndPacket packet) { -@@ -2115,4 +3667,17 @@ +@@ -2115,4 +3657,17 @@ InteractionResult run(ServerPlayer player, Entity entity, InteractionHand hand); } diff --git a/paper-server/src/main/java/io/papermc/paper/brigadier/NullCommandSender.java b/paper-server/src/main/java/io/papermc/paper/brigadier/NullCommandSender.java new file mode 100644 index 0000000000..367ef7e076 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/io/papermc/paper/brigadier/PaperBrigadierProviderImpl.java b/paper-server/src/main/java/io/papermc/paper/brigadier/PaperBrigadierProviderImpl.java deleted file mode 100644 index dd6012b6a0..0000000000 --- a/paper-server/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/paper-server/src/main/java/io/papermc/paper/command/brigadier/ApiMirrorRootNode.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/ApiMirrorRootNode.java new file mode 100644 index 0000000000..74d7b19627 --- /dev/null +++ b/paper-server/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. + *

+ * 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."); + } + + // 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 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. + *

+ * 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() + .forward(node.getRedirect() == null ? null : this.unwrapNode(node.getRedirect()), node.getRedirectModifier(), node.isFork()) + .build(); + } + +} diff --git a/paper-server/src/main/java/io/papermc/paper/command/brigadier/MessageComponentSerializerImpl.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/MessageComponentSerializerImpl.java new file mode 100644 index 0000000000..0b33c6cf23 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/io/papermc/paper/command/brigadier/PaperBrigadier.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/PaperBrigadier.java new file mode 100644 index 0000000000..4acf7c3bcf --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/io/papermc/paper/command/brigadier/PaperCommandSourceStack.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/PaperCommandSourceStack.java new file mode 100644 index 0000000000..1b1642f306 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/io/papermc/paper/command/brigadier/PaperCommands.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/PaperCommands.java new file mode 100644 index 0000000000..95d3b42cbe --- /dev/null +++ b/paper-server/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 { + + 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, boolean override) { + final @Nullable CommandNode 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 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) + .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 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/paper-server/src/main/java/io/papermc/paper/command/brigadier/PluginCommandNode.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/PluginCommandNode.java new file mode 100644 index 0000000000..3a9f58873b --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/io/papermc/paper/command/brigadier/PluginVanillaCommandWrapper.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/PluginVanillaCommandWrapper.java new file mode 100644 index 0000000000..cf8359af60 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/io/papermc/paper/command/brigadier/ShadowBrigNode.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/ShadowBrigNode.java new file mode 100644 index 0000000000..895addef90 --- /dev/null +++ b/paper-server/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/paper-server/src/main/java/io/papermc/paper/command/brigadier/argument/SignedMessageResolverImpl.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/argument/SignedMessageResolverImpl.java new file mode 100644 index 0000000000..07a23be2cd --- /dev/null +++ b/paper-server/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 resolveSignedMessage(final String argumentName, final CommandContext erased) throws CommandSyntaxException { + final CompletableFuture future = new CompletableFuture<>(); + + final MessageArgument.Message response = ((CommandContext) erased).getArgument(argumentName, SignedMessageResolverImpl.class).message; + MessageArgument.resolveChatMessage(response, erased, argumentName, (message) -> { + future.complete(message.adventureView()); + }); + return future; + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/command/brigadier/argument/VanillaArgumentProviderImpl.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/argument/VanillaArgumentProviderImpl.java new file mode 100644 index 0000000000..38fb7d13ab --- /dev/null +++ b/paper-server/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 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 MCUtil.toPosition(pos); + }); + } + + @Override + public ArgumentType 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() { + 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