diff --git a/.editorconfig b/.editorconfig index 600eac0fb910..8177de6f9fa0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -36,7 +36,7 @@ ij_java_method_parameters_right_paren_on_new_line = true ij_java_use_fq_class_names = false ij_java_class_names_in_javadoc = 1 -[paper-server/src/minecraft/java/**/*.java] +[paper-server/src/minecraft/java/{net/minecraft,com/mojang}/**/*.java] ij_java_use_fq_class_names = true [paper-server/src/minecraft/resources/data/**/*.json] diff --git a/paper-api/src/main/java/org/bukkit/event/HandlerList.java b/paper-api/src/main/java/org/bukkit/event/HandlerList.java index ef9d9f3ddaf2..cd6a8f4d543a 100644 --- a/paper-api/src/main/java/org/bukkit/event/HandlerList.java +++ b/paper-api/src/main/java/org/bukkit/event/HandlerList.java @@ -5,7 +5,9 @@ import java.util.EnumMap; import java.util.List; import java.util.ListIterator; +import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.RegisteredListener; import org.jetbrains.annotations.NotNull; @@ -36,7 +38,8 @@ public class HandlerList { /** * Event types which have instantiated a {@link HandlerList}. */ - private static final java.util.Set EVENT_TYPES = java.util.concurrent.ConcurrentHashMap.newKeySet(); + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private static final Map EVENT_TYPES = new ConcurrentHashMap<>(); /** * Bake all handler lists. Best used just after all normal event @@ -102,7 +105,7 @@ public HandlerList() { java.lang.StackWalker.getInstance(java.util.EnumSet.of(java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE), 4) .walk(s -> s.filter(f -> Event.class.isAssignableFrom(f.getDeclaringClass())).findFirst()) .map(f -> f.getDeclaringClass().getName()) - .ifPresent(EVENT_TYPES::add); + .ifPresent(name -> EVENT_TYPES.put(name, this)); handlerslots = new EnumMap<>(EventPriority.class); for (EventPriority o : EventPriority.values()) { diff --git a/paper-server/patches/features/0001-Moonrise-optimisation-patches.patch b/paper-server/patches/features/0001-Moonrise-optimisation-patches.patch index 9de3cccbff3e..c3c06ea9a182 100644 --- a/paper-server/patches/features/0001-Moonrise-optimisation-patches.patch +++ b/paper-server/patches/features/0001-Moonrise-optimisation-patches.patch @@ -22256,20 +22256,20 @@ index 0000000000000000000000000000000000000000..95154636727366adfb4fb67e7db8b09f + private SaveUtil() {} +} diff --git a/io/papermc/paper/FeatureHooks.java b/io/papermc/paper/FeatureHooks.java -index 61c526263cfbd1871f6b8548ed4431a76ee95a31..485413cb6b20c4bd20fbc29a0db43c051993fe79 100644 +index 619e981bc08b5f3c69868f15e559ff1bb9e52959..8e7f8f25925586634bba9310e882ba40d48ba5ad 100644 --- a/io/papermc/paper/FeatureHooks.java +++ b/io/papermc/paper/FeatureHooks.java -@@ -1,6 +1,9 @@ - package io.papermc.paper; +@@ -2,6 +2,9 @@ package io.papermc.paper; - import io.papermc.paper.command.PaperSubcommand; + import com.mojang.brigadier.builder.LiteralArgumentBuilder; + import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.subcommands.ChunkDebugCommand; +import io.papermc.paper.command.subcommands.FixLightCommand; +import it.unimi.dsi.fastutil.longs.LongIterator; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongSet; import it.unimi.dsi.fastutil.longs.LongSets; -@@ -32,13 +35,16 @@ public final class FeatureHooks { +@@ -33,13 +36,20 @@ public final class FeatureHooks { // this includes non-accessible entities public static Iterable getAllEntities(final net.minecraft.server.level.ServerLevel world) { @@ -22281,13 +22281,17 @@ index 61c526263cfbd1871f6b8548ed4431a76ee95a31..485413cb6b20c4bd20fbc29a0db43c05 + ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.setUnloadDelay(ticks); // Paper - rewrite chunk system } - public static void registerPaperCommands(final Map, PaperSubcommand> commands) { -+ commands.put(Set.of("fixlight"), new FixLightCommand()); // Paper - rewrite chunk system -+ commands.put(Set.of("debug", "chunkinfo", "holderinfo"), new ChunkDebugCommand()); // Paper - rewrite chunk system + public static void registerPaperCommands(final List> commands) { ++ // Paper start - Chunk system ++ commands.add(FixLightCommand.create()); ++ commands.add(ChunkDebugCommand.createDebug()); ++ commands.add(ChunkDebugCommand.createChunkInfo()); ++ commands.add(ChunkDebugCommand.createHolderInfo()); ++ // Paper end - Chunk system } public static LevelChunkSection createSection(final PalettedContainerFactory palettedContainerFactory, final Level level, final ChunkPos chunkPos, final int chunkSection) { -@@ -59,111 +65,58 @@ public final class FeatureHooks { +@@ -60,111 +70,58 @@ public final class FeatureHooks { } public static Set getSentChunkKeys(final ServerPlayer player) { @@ -22420,7 +22424,7 @@ index 61c526263cfbd1871f6b8548ed4431a76ee95a31..485413cb6b20c4bd20fbc29a0db43c05 org.bukkit.Chunk chunk = null; for (net.minecraft.server.level.Ticket ticket : tickets) { -@@ -183,15 +136,15 @@ public final class FeatureHooks { +@@ -184,15 +141,15 @@ public final class FeatureHooks { } public static int getViewDistance(net.minecraft.server.level.ServerLevel world) { @@ -22439,7 +22443,7 @@ index 61c526263cfbd1871f6b8548ed4431a76ee95a31..485413cb6b20c4bd20fbc29a0db43c05 } public static void setViewDistance(net.minecraft.server.level.ServerLevel world, int distance) { -@@ -209,35 +162,31 @@ public final class FeatureHooks { +@@ -210,35 +167,31 @@ public final class FeatureHooks { } public static void setSendViewDistance(net.minecraft.server.level.ServerLevel world, int distance) { @@ -22481,28 +22485,31 @@ index 61c526263cfbd1871f6b8548ed4431a76ee95a31..485413cb6b20c4bd20fbc29a0db43c05 + ((ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer)player).moonrise$getViewDistanceHolder().setSendViewDistance(distance); // Paper - rewrite chunk system } --} + } \ No newline at end of file -+} diff --git a/io/papermc/paper/command/subcommands/ChunkDebugCommand.java b/io/papermc/paper/command/subcommands/ChunkDebugCommand.java new file mode 100644 -index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd01fba0fc +index 0000000000000000000000000000000000000000..cc77c279492a0506e1b4d3447879a0b6983ace71 --- /dev/null +++ b/io/papermc/paper/command/subcommands/ChunkDebugCommand.java -@@ -0,0 +1,277 @@ +@@ -0,0 +1,266 @@ +package io.papermc.paper.command.subcommands; + +import ca.spottedleaf.moonrise.common.util.JsonUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler; +import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder; -+import io.papermc.paper.command.CommandUtil; -+import io.papermc.paper.command.PaperSubcommand; ++import com.mojang.brigadier.Command; ++import com.mojang.brigadier.builder.LiteralArgumentBuilder; ++import io.papermc.paper.command.PaperCommand; ++import io.papermc.paper.command.brigadier.CommandSourceStack; ++import io.papermc.paper.command.brigadier.Commands; ++import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import java.io.File; -+import java.util.ArrayList; -+import java.util.Collections; +import java.util.List; -+import java.util.Locale; ++import java.util.concurrent.atomic.AtomicInteger; ++import java.util.function.ToIntBiFunction; ++import net.kyori.adventure.text.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.chunk.ChunkAccess; @@ -22510,11 +22517,11 @@ index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.ProtoChunk; +import org.bukkit.Bukkit; ++import org.bukkit.World; +import org.bukkit.command.CommandSender; ++import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.craftbukkit.CraftWorld; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; ++import org.jspecify.annotations.NullMarked; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.BLUE; @@ -22522,73 +22529,81 @@ index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd +import static net.kyori.adventure.text.format.NamedTextColor.GREEN; +import static net.kyori.adventure.text.format.NamedTextColor.RED; + -+@DefaultQualifier(NonNull.class) -+public final class ChunkDebugCommand implements PaperSubcommand { -+ @Override -+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { -+ switch (subCommand) { -+ case "debug" -> this.doDebug(sender, args); -+ case "chunkinfo" -> this.doChunkInfo(sender, args); -+ case "holderinfo" -> this.doHolderInfo(sender, args); -+ } -+ return true; ++@NullMarked ++public final class ChunkDebugCommand { ++ ++ private static final Component DEBUG_HELP = text("Use /paper debug chunks to dump loaded chunk information to a file", RED); ++ private static final AtomicInteger ASYNC_DEBUG_CHUNKS_COUNT = new AtomicInteger(); ++ ++ public static LiteralArgumentBuilder createDebug() { ++ return Commands.literal("debug") ++ .requires(PaperCommand.hasPermission("debug")) ++ .executes(context -> { ++ context.getSource().getSender().sendMessage(DEBUG_HELP); ++ return Command.SINGLE_SUCCESS; ++ }) ++ .then(Commands.literal("help") ++ .executes(context -> { ++ context.getSource().getSender().sendMessage(DEBUG_HELP); ++ return Command.SINGLE_SUCCESS; ++ }) ++ ) ++ .then(Commands.literal("chunks") ++ .executes(context -> { ++ return debugDumpLoadedChunks(context.getSource().getSender()); ++ }) ++ .then(Commands.literal("--async") ++ .requires(source -> source.getSender() instanceof ConsoleCommandSender) ++ .executes(context -> { ++ context.getSource().getSender().sendPlainMessage("Scheduling async debug chunks"); ++ Thread.ofPlatform().name("Async debug thread #" + ASYNC_DEBUG_CHUNKS_COUNT.getAndIncrement()).daemon(true).start(() -> { ++ context.getSource().getSender().sendPlainMessage("Async debug chunks executing"); ++ ChunkTaskScheduler.dumpAllChunkLoadInfo(MinecraftServer.getServer(), false); ++ debugDumpLoadedChunks(context.getSource().getSender()); ++ }); ++ return Command.SINGLE_SUCCESS; ++ }) ++ ) ++ ); + } + -+ @Override -+ public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { -+ switch (subCommand) { -+ case "debug" -> { -+ if (args.length == 1) { -+ return CommandUtil.getListMatchingLast(sender, args, "help", "chunks"); -+ } -+ } -+ case "holderinfo" -> { -+ List worldNames = new ArrayList<>(); -+ worldNames.add("*"); -+ for (org.bukkit.World world : Bukkit.getWorlds()) { -+ worldNames.add(world.getName()); -+ } -+ if (args.length == 1) { -+ return CommandUtil.getListMatchingLast(sender, args, worldNames); -+ } -+ } -+ case "chunkinfo" -> { -+ List worldNames = new ArrayList<>(); -+ worldNames.add("*"); -+ for (org.bukkit.World world : Bukkit.getWorlds()) { -+ worldNames.add(world.getName()); -+ } -+ if (args.length == 1) { -+ return CommandUtil.getListMatchingLast(sender, args, worldNames); -+ } -+ } -+ } -+ return Collections.emptyList(); ++ private static LiteralArgumentBuilder createWorldInfo(String name, ToIntBiFunction> action) { ++ return Commands.literal(name) ++ .requires(PaperCommand.hasPermission(name)) ++ .executes(context -> { ++ return action.applyAsInt( ++ context.getSource().getSender(), ++ context.getSource().getSender() instanceof ConsoleCommandSender ? Bukkit.getWorlds() : List.of(context.getSource().getLocation().getWorld()) ++ ); ++ }) ++ .then(Commands.literal("*") ++ .executes(context -> { ++ return action.applyAsInt(context.getSource().getSender(), Bukkit.getWorlds()); ++ }) ++ ) ++ .then(Commands.argument("world", ArgumentTypes.world()) ++ .executes(context -> { ++ return action.applyAsInt(context.getSource().getSender(), List.of(context.getArgument("world", World.class))); ++ }) ++ ); + } + -+ private void doChunkInfo(final CommandSender sender, final String[] args) { -+ List worlds; -+ if (args.length < 1 || args[0].equals("*")) { -+ worlds = Bukkit.getWorlds(); -+ } else { -+ worlds = new ArrayList<>(args.length); -+ for (final String arg : args) { -+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg); -+ if (world == null) { -+ sender.sendMessage(text("World '" + arg + "' is invalid", RED)); -+ return; -+ } -+ worlds.add(world); -+ } -+ } ++ public static LiteralArgumentBuilder createChunkInfo() { ++ return createWorldInfo("chunkinfo", ChunkDebugCommand::doChunkInfo); ++ } + ++ public static LiteralArgumentBuilder createHolderInfo() { ++ return createWorldInfo("holderinfo", ChunkDebugCommand::doHolderInfo); ++ } ++ ++ private static int doChunkInfo(final CommandSender sender, final List worlds) { + int accumulatedTotal = 0; + int accumulatedInactive = 0; + int accumulatedBorder = 0; + int accumulatedTicking = 0; + int accumulatedEntityTicking = 0; + -+ for (final org.bukkit.World bukkitWorld : worlds) { ++ for (final World bukkitWorld : worlds) { + final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); + + int total = 0; @@ -22601,7 +22616,7 @@ index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd + final NewChunkHolder.ChunkCompletion completion = holder.getLastChunkCompletion(); + final ChunkAccess chunk = completion == null ? null : completion.chunk(); + -+ if (!(chunk instanceof LevelChunk fullChunk)) { ++ if (!(chunk instanceof LevelChunk)) { + continue; + } + @@ -22633,7 +22648,7 @@ index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd + accumulatedTicking += blockTicking; + accumulatedEntityTicking += entityTicking; + -+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":"))); ++ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.key().asString(), GREEN), text(":"))); + sender.sendMessage(text().color(DARK_AQUA).append( + text("Total: ", BLUE), text(total), + text(" Inactive: ", BLUE), text(inactive), @@ -22652,24 +22667,10 @@ index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd + text(" Entity Ticking: ", BLUE), text(accumulatedEntityTicking) + )); + } ++ return worlds.size(); + } + -+ private void doHolderInfo(final CommandSender sender, final String[] args) { -+ List worlds; -+ if (args.length < 1 || args[0].equals("*")) { -+ worlds = Bukkit.getWorlds(); -+ } else { -+ worlds = new ArrayList<>(args.length); -+ for (final String arg : args) { -+ org.bukkit.@Nullable World world = Bukkit.getWorld(arg); -+ if (world == null) { -+ sender.sendMessage(text("World '" + arg + "' is invalid", RED)); -+ return; -+ } -+ worlds.add(world); -+ } -+ } -+ ++ private static int doHolderInfo(final CommandSender sender, final List worlds) { + int accumulatedTotal = 0; + int accumulatedCanUnload = 0; + int accumulatedNull = 0; @@ -22677,7 +22678,7 @@ index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd + int accumulatedProtoChunk = 0; + int accumulatedFullChunk = 0; + -+ for (final org.bukkit.World bukkitWorld : worlds) { ++ for (final World bukkitWorld : worlds) { + final ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); + + int total = 0; @@ -22715,7 +22716,7 @@ index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd + accumulatedProtoChunk += protoChunk; + accumulatedFullChunk += fullChunk; + -+ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.getName(), GREEN), text(":"))); ++ sender.sendMessage(text().append(text("Chunks in ", BLUE), text(bukkitWorld.key().asString(), GREEN), text(":"))); + sender.sendMessage(text().color(DARK_AQUA).append( + text("Total: ", BLUE), text(total), + text(" Unloadable: ", BLUE), text(canUnload), @@ -22736,128 +22737,116 @@ index 0000000000000000000000000000000000000000..2dca7afbd93cfbb8686f336fcd3b45dd + text(" Full: ", BLUE), text(accumulatedFullChunk) + )); + } ++ return worlds.size(); + } + -+ private void doDebug(final CommandSender sender, final String[] args) { -+ if (args.length < 1) { -+ sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED)); -+ return; -+ } ++ private static int debugDumpLoadedChunks(final CommandSender sender) { ++ final File file = ChunkTaskScheduler.getChunkDebugFile(); ++ sender.sendMessage( ++ text("Writing chunk information dump to", GREEN) ++ .appendSpace() ++ .append(PaperCommand.asFriendlyPath(file.toPath())) ++ ); + -+ final String debugType = args[0].toLowerCase(Locale.ROOT); -+ switch (debugType) { -+ case "chunks" -> { -+ if (args.length >= 2 && args[1].toLowerCase(Locale.ROOT).equals("help")) { -+ sender.sendMessage(text("Use /paper debug chunks to dump loaded chunk information to a file", RED)); -+ break; -+ } -+ final File file = ChunkTaskScheduler.getChunkDebugFile(); -+ sender.sendMessage(text("Writing chunk information dump to " + file, GREEN)); -+ try { -+ JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(MinecraftServer.getServer()), file); -+ sender.sendMessage(text("Successfully written chunk information!", GREEN)); -+ } catch (Throwable thr) { -+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); -+ sender.sendMessage(text("Failed to dump chunk information, see console", RED)); -+ } -+ } -+ // "help" & default -+ default -> sender.sendMessage(text("Use /paper debug [chunks] help for more information on a specific command", RED)); ++ try { ++ JsonUtil.writeJson(ChunkTaskScheduler.debugAllWorlds(MinecraftServer.getServer()), file); ++ sender.sendMessage(text("Successfully written chunk information!", GREEN)); ++ return Command.SINGLE_SUCCESS; ++ } catch (Throwable thr) { ++ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file {}", file, thr); ++ sender.sendMessage(text("Failed to dump chunk information, see console", RED)); ++ return 0; + } + } -+ +} diff --git a/io/papermc/paper/command/subcommands/FixLightCommand.java b/io/papermc/paper/command/subcommands/FixLightCommand.java new file mode 100644 -index 0000000000000000000000000000000000000000..85950a1aa732ab8c01ad28bec9e0de140e1a172e +index 0000000000000000000000000000000000000000..ce31e1c2b1f7a03939f3a287b27db003d52a42da --- /dev/null +++ b/io/papermc/paper/command/subcommands/FixLightCommand.java -@@ -0,0 +1,116 @@ +@@ -0,0 +1,110 @@ +package io.papermc.paper.command.subcommands; + +import ca.spottedleaf.moonrise.patches.starlight.light.StarLightLightingProvider; -+import io.papermc.paper.command.PaperSubcommand; ++import com.mojang.brigadier.Command; ++import com.mojang.brigadier.arguments.IntegerArgumentType; ++import com.mojang.brigadier.builder.LiteralArgumentBuilder; ++import io.papermc.paper.command.PaperCommand; ++import io.papermc.paper.command.brigadier.CommandSourceStack; ++import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.util.MCUtil; ++import java.text.DecimalFormat; ++import java.util.Iterator; ++import java.util.LinkedHashSet; ++import java.util.function.Consumer; ++import net.kyori.adventure.text.ComponentLike; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ThreadedLevelLightEngine; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; ++import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+import java.text.DecimalFormat; ++import org.jspecify.annotations.NullMarked; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.BLUE; +import static net.kyori.adventure.text.format.NamedTextColor.DARK_AQUA; -+import static net.kyori.adventure.text.format.NamedTextColor.RED; + -+@DefaultQualifier(NonNull.class) -+public final class FixLightCommand implements PaperSubcommand { ++@NullMarked ++public final class FixLightCommand { + ++ private static final int MAX_RADIUS = 32; + private static final ThreadLocal ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> { + return new DecimalFormat("#,##0.0"); + }); + -+ @Override -+ public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { -+ this.doFixLight(sender, args); -+ return true; ++ public static LiteralArgumentBuilder create() { ++ return Commands.literal("fixlight") ++ .requires(PaperCommand.hasPermission("fixlight")) ++ .executes(context -> { ++ if (!(context.getSource().getExecutor() instanceof Player player)) { ++ throw net.minecraft.commands.CommandSourceStack.ERROR_NOT_PLAYER.create(); ++ } ++ doFixLight(player, 2, context.getSource().getSender()); ++ return Command.SINGLE_SUCCESS; ++ }) ++ .then(Commands.argument("radius", IntegerArgumentType.integer(0, MAX_RADIUS)) ++ .executes(context -> { ++ if (!(context.getSource().getExecutor() instanceof Player player)) { ++ throw net.minecraft.commands.CommandSourceStack.ERROR_NOT_PLAYER.create(); ++ } ++ doFixLight(player, IntegerArgumentType.getInteger(context, "radius"), context.getSource().getSender()); ++ return Command.SINGLE_SUCCESS; ++ }) ++ ); + } + -+ private void doFixLight(final CommandSender sender, final String[] args) { -+ if (!(sender instanceof Player)) { -+ sender.sendMessage(text("Only players can use this command", RED)); -+ return; -+ } -+ @Nullable Runnable post = null; -+ int radius = 2; -+ if (args.length > 0) { -+ try { -+ final int parsed = Integer.parseInt(args[0]); -+ if (parsed < 0) { -+ sender.sendMessage(text("Radius cannot be negative!", RED)); -+ return; -+ } -+ final int maxRadius = 32; -+ radius = Math.min(maxRadius, parsed); -+ if (radius != parsed) { -+ post = () -> sender.sendMessage(text("Radius '" + parsed + "' was not in the required range [0, " + maxRadius + "], it was lowered to the maximum (" + maxRadius + " chunks).", RED)); -+ } -+ } catch (final Exception e) { -+ sender.sendMessage(text("'" + args[0] + "' is not a valid number.", RED)); -+ return; -+ } -+ } -+ -+ CraftPlayer player = (CraftPlayer) sender; -+ ServerPlayer handle = player.getHandle(); -+ ServerLevel world = (ServerLevel) handle.level(); -+ ThreadedLevelLightEngine lightengine = world.getChunkSource().getLightEngine(); -+ this.starlightFixLight(handle, world, lightengine, radius, post); ++ private static void doFixLight(final Player fromPlayer, final int radius, final CommandSender sender) { ++ ServerPlayer player = ((CraftPlayer) fromPlayer).getHandle(); ++ ServerLevel world = player.level(); ++ ThreadedLevelLightEngine lightEngine = world.getChunkSource().getLightEngine(); ++ starlightFixLight(player, world, lightEngine, radius, sender::sendMessage); + } + -+ private void starlightFixLight( -+ final ServerPlayer sender, ++ private static void starlightFixLight( ++ final ServerPlayer player, + final ServerLevel world, -+ final ThreadedLevelLightEngine lightengine, ++ final ThreadedLevelLightEngine lightEngine, + final int radius, -+ final @Nullable Runnable done ++ final Consumer output + ) { + final long start = System.nanoTime(); -+ final java.util.LinkedHashSet chunks = new java.util.LinkedHashSet<>(MCUtil.getSpiralOutChunks(sender.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos ++ final LinkedHashSet chunks = new LinkedHashSet<>(MCUtil.getSpiralOutChunks(player.blockPosition(), radius)); // getChunkCoordinates is actually just bad mappings, this function rets position as blockpos + + final int[] pending = new int[1]; -+ for (java.util.Iterator iterator = chunks.iterator(); iterator.hasNext(); ) { ++ for (Iterator iterator = chunks.iterator(); iterator.hasNext(); ) { + final ChunkPos chunkPos = iterator.next(); + -+ final @Nullable ChunkAccess chunk = (ChunkAccess) world.getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z); -+ if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.LIGHT)) { ++ final ChunkAccess chunk = (ChunkAccess) world.getChunkSource().getChunkForLighting(chunkPos.x, chunkPos.z); ++ if (chunk == null || !chunk.isLightCorrect() || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) { + // cannot relight this chunk + iterator.remove(); + continue; @@ -22867,26 +22856,23 @@ index 0000000000000000000000000000000000000000..85950a1aa732ab8c01ad28bec9e0de14 + } + + final int[] relitChunks = new int[1]; -+ ((StarLightLightingProvider)lightengine).starlight$serverRelightChunks(chunks, ++ ((StarLightLightingProvider) lightEngine).starlight$serverRelightChunks(chunks, + (final ChunkPos chunkPos) -> { + ++relitChunks[0]; -+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( ++ output.accept(text().color(DARK_AQUA).append( + text("Relit chunk ", BLUE), text(chunkPos.toString()), + text(", progress: ", BLUE), text(ONE_DECIMAL_PLACES.get().format(100.0 * (double) (relitChunks[0]) / (double) pending[0]) + "%") + )); + }, + (final int totalRelit) -> { + final long end = System.nanoTime(); -+ sender.getBukkitEntity().sendMessage(text().color(DARK_AQUA).append( ++ output.accept(text().color(DARK_AQUA).append( + text("Relit ", BLUE), text(totalRelit), + text(" chunks. Took ", BLUE), text(ONE_DECIMAL_PLACES.get().format(1.0e-6 * (end - start)) + "ms") + )); -+ if (done != null) { -+ done.run(); -+ } + } + ); -+ sender.getBukkitEntity().sendMessage(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks"))); ++ output.accept(text().color(BLUE).append(text("Relighting "), text(pending[0], DARK_AQUA), text(" chunks"))); + } +} diff --git a/io/papermc/paper/threadedregions/TickRegions.java b/io/papermc/paper/threadedregions/TickRegions.java @@ -23315,44 +23301,6 @@ index cab1ab613dd081e9472de515a19e1b4738e4fba3..96f7f37c42cecea1d714f7b16276f430 // CraftBukkit start public boolean isDebugging() { -diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java -index 294ad63a3ec518fd26e6f82c005ecfd6660ddd4f..eb06d8f012684845146429832e977e6c1ddcd62b 100644 ---- a/net/minecraft/server/dedicated/DedicatedServer.java -+++ b/net/minecraft/server/dedicated/DedicatedServer.java -@@ -550,7 +550,33 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface - this.handleConsoleInputs(); - } - -+ private static final java.util.concurrent.atomic.AtomicInteger ASYNC_DEBUG_CHUNKS_COUNT = new java.util.concurrent.atomic.AtomicInteger(); // Paper - rewrite chunk system -+ - public void handleConsoleInput(String msg, CommandSourceStack source) { -+ // Paper start - rewrite chunk system -+ if (msg.equalsIgnoreCase("paper debug chunks --async")) { -+ LOGGER.info("Scheduling async debug chunks"); -+ Runnable run = () -> { -+ LOGGER.info("Async debug chunks executing"); -+ ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.dumpAllChunkLoadInfo(this, false); -+ org.bukkit.command.CommandSender sender = MinecraftServer.getServer().console; -+ java.io.File file = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.getChunkDebugFile(); -+ sender.sendMessage(net.kyori.adventure.text.Component.text("Writing chunk information dump to " + file, net.kyori.adventure.text.format.NamedTextColor.GREEN)); -+ try { -+ ca.spottedleaf.moonrise.common.util.JsonUtil.writeJson(ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler.debugAllWorlds(this), file); -+ sender.sendMessage(net.kyori.adventure.text.Component.text("Successfully written chunk information!", net.kyori.adventure.text.format.NamedTextColor.GREEN)); -+ } catch (Throwable thr) { -+ MinecraftServer.LOGGER.warn("Failed to dump chunk information to file " + file.toString(), thr); -+ sender.sendMessage(net.kyori.adventure.text.Component.text("Failed to dump chunk information, see console", net.kyori.adventure.text.format.NamedTextColor.RED)); -+ } -+ }; -+ Thread t = new Thread(run); -+ t.setName("Async debug thread #" + ASYNC_DEBUG_CHUNKS_COUNT.getAndIncrement()); -+ t.setDaemon(true); -+ t.start(); -+ return; -+ } -+ // Paper end - rewrite chunk system - this.serverCommandQueue.add(new ConsoleInput(msg, source)); // Paper - Perf: use proper queue - } - diff --git a/net/minecraft/server/level/ChunkHolder.java b/net/minecraft/server/level/ChunkHolder.java index e8f81fa17a0470ae9bf3f506fc7fb10fb7810cc7..22123d8b0fd8741c6df4efaec56f3c694e0529a3 100644 --- a/net/minecraft/server/level/ChunkHolder.java @@ -31715,7 +31663,7 @@ index 9dc13a5b6cc1afc118a261802c71da25ae577fe5..a49a06662de4062a77112e358f536d45 public static ProblemReporter.PathElement problemPath(ChunkPos pos) { diff --git a/net/minecraft/world/level/chunk/ChunkGenerator.java b/net/minecraft/world/level/chunk/ChunkGenerator.java -index 2acee7d7211c4e7b9ac02e9a48d6c3ea239e6949..78600f4c583056403c93e72ec0996b00ec6d1284 100644 +index faf8f53bdcaa69fbbd816739dd4f1e812ced2165..7b98964eb77a619bdaaf5a9d331e3cfe080b52a4 100644 --- a/net/minecraft/world/level/chunk/ChunkGenerator.java +++ b/net/minecraft/world/level/chunk/ChunkGenerator.java @@ -116,7 +116,7 @@ public abstract class ChunkGenerator { diff --git a/paper-server/patches/features/0029-Anti-Xray.patch b/paper-server/patches/features/0029-Anti-Xray.patch index 532e5b76a2e6..fc7ab06dd242 100644 --- a/paper-server/patches/features/0029-Anti-Xray.patch +++ b/paper-server/patches/features/0029-Anti-Xray.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Anti-Xray diff --git a/io/papermc/paper/FeatureHooks.java b/io/papermc/paper/FeatureHooks.java -index 485413cb6b20c4bd20fbc29a0db43c051993fe79..22f37a0779996c9e3a9416a2a1ba2bee74e3f568 100644 +index aabeb29a7f3d593ad78d47cb6baa932f0dffa5aa..5464eb75e1bbf2962491fcc13bcfe3d771644f79 100644 --- a/io/papermc/paper/FeatureHooks.java +++ b/io/papermc/paper/FeatureHooks.java -@@ -48,20 +48,25 @@ public final class FeatureHooks { +@@ -53,20 +53,25 @@ public final class FeatureHooks { } public static LevelChunkSection createSection(final PalettedContainerFactory palettedContainerFactory, final Level level, final ChunkPos chunkPos, final int chunkSection) { @@ -184,7 +184,7 @@ index c65b274b965b95eae33690e63c5da2d5a9f2981a..644948d64791d0ffa4166375d0f4419f if (io.papermc.paper.event.packet.PlayerChunkLoadEvent.getHandlerList().getRegisteredListeners().length > 0) { new io.papermc.paper.event.packet.PlayerChunkLoadEvent(new org.bukkit.craftbukkit.CraftChunk(chunk), packetListener.getPlayer().getBukkitEntity()).callEvent(); diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java -index a17d021269c65d98a2ba847c0059a421ea051863..9f86d1fc4ea4c6987fa207644573565aea7edead 100644 +index dec9dd8e42f90ccf7e5e3ad945e459d07159250d..989ac565c47a70c7947cb7315d0f5c2cfecd0363 100644 --- a/net/minecraft/server/players/PlayerList.java +++ b/net/minecraft/server/players/PlayerList.java @@ -329,7 +329,7 @@ public abstract class PlayerList { diff --git a/paper-server/patches/sources/io/papermc/paper/FeatureHooks.java.patch b/paper-server/patches/sources/io/papermc/paper/FeatureHooks.java.patch index dd764f0757aa..63fb271ce366 100644 --- a/paper-server/patches/sources/io/papermc/paper/FeatureHooks.java.patch +++ b/paper-server/patches/sources/io/papermc/paper/FeatureHooks.java.patch @@ -1,9 +1,10 @@ --- /dev/null +++ b/io/papermc/paper/FeatureHooks.java -@@ -1,0 +_,243 @@ +@@ -1,0 +_,244 @@ +package io.papermc.paper; + -+import io.papermc.paper.command.PaperSubcommand; ++import com.mojang.brigadier.builder.LiteralArgumentBuilder; ++import io.papermc.paper.command.brigadier.CommandSourceStack; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import it.unimi.dsi.fastutil.longs.LongSets; @@ -41,7 +42,7 @@ + public static void setPlayerChunkUnloadDelay(final long ticks) { + } + -+ public static void registerPaperCommands(final Map, PaperSubcommand> commands) { ++ public static void registerPaperCommands(final List> commands) { + } + + public static LevelChunkSection createSection(final PalettedContainerFactory palettedContainerFactory, final Level level, final ChunkPos chunkPos, final int chunkSection) { 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 c8cd84714454..27c9fe3c266d 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 @@ -112,7 +112,7 @@ + // Paper end - fix converting txt to json file + org.spigotmc.WatchdogThread.doStart(org.spigotmc.SpigotConfig.timeoutTime, org.spigotmc.SpigotConfig.restartOnCrash); // Paper - start watchdog thread + thread.start(); // Paper - Enhance console tab completions for brigadier commands; start console thread after MinecraftServer.console & PaperConfig are initialized -+ io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command ++ io.papermc.paper.command.PaperCommands.registerLegacyCommands(this); // Paper - setup legacy command + this.server.spark.registerCommandBeforePlugins(this.server); // Paper - spark + com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics + com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now diff --git a/paper-server/src/main/java/io/papermc/paper/command/CommandUtil.java b/paper-server/src/main/java/io/papermc/paper/command/CommandUtil.java deleted file mode 100644 index a9443f24971f..000000000000 --- a/paper-server/src/main/java/io/papermc/paper/command/CommandUtil.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.papermc.paper.command; - -import com.google.common.base.Functions; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import net.minecraft.resources.Identifier; -import org.bukkit.command.CommandSender; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; - -@DefaultQualifier(NonNull.class) -public final class CommandUtil { - private CommandUtil() { - } - - // Code from Mojang - copyright them - public static List getListMatchingLast( - final CommandSender sender, - final String[] args, - final String... matches - ) { - return getListMatchingLast(sender, args, Arrays.asList(matches)); - } - - public static boolean matches(final String s, final String s1) { - return s1.regionMatches(true, 0, s, 0, s.length()); - } - - public static List getListMatchingLast( - final CommandSender sender, - final String[] strings, - final Collection collection - ) { - String last = strings[strings.length - 1]; - ArrayList results = Lists.newArrayList(); - - if (!collection.isEmpty()) { - Iterator iterator = Iterables.transform(collection, Functions.toStringFunction()).iterator(); - - while (iterator.hasNext()) { - String s1 = (String) iterator.next(); - - if (matches(last, s1) && (sender.hasPermission(PaperCommand.BASE_PERM + s1) || sender.hasPermission("bukkit.command.paper"))) { - results.add(s1); - } - } - - if (results.isEmpty()) { - iterator = collection.iterator(); - - while (iterator.hasNext()) { - Object object = iterator.next(); - - if (object instanceof Identifier && matches(last, ((Identifier) object).getPath())) { - results.add(String.valueOf(object)); - } - } - } - } - - return results; - } - // end copy stuff -} diff --git a/paper-server/src/main/java/io/papermc/paper/command/PaperCommand.java b/paper-server/src/main/java/io/papermc/paper/command/PaperCommand.java index b629a9a37ce7..c04b174692be 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/PaperCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/PaperCommand.java @@ -1,6 +1,11 @@ package io.papermc.paper.command; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.tree.LiteralCommandNode; import io.papermc.paper.FeatureHooks; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; import io.papermc.paper.command.subcommands.DumpItemCommand; import io.papermc.paper.command.subcommands.DumpListenersCommand; import io.papermc.paper.command.subcommands.DumpPluginsCommand; @@ -10,149 +15,81 @@ import io.papermc.paper.command.subcommands.ReloadCommand; import io.papermc.paper.command.subcommands.SyncLoadInfoCommand; import io.papermc.paper.command.subcommands.VersionCommand; -import it.unimi.dsi.fastutil.Pair; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.nio.file.Path; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; import net.minecraft.util.Util; import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; import org.bukkit.plugin.PluginManager; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.framework.qual.DefaultQualifier; +import org.jspecify.annotations.NullMarked; import static net.kyori.adventure.text.Component.text; -import static net.kyori.adventure.text.format.NamedTextColor.RED; -@DefaultQualifier(NonNull.class) -public final class PaperCommand extends Command { - static final String BASE_PERM = "bukkit.command.paper."; - // subcommand label -> subcommand - private static final Map SUBCOMMANDS = Util.make(() -> { - final Map, PaperSubcommand> commands = new HashMap<>(); +@NullMarked +public final class PaperCommand { - commands.put(Set.of("heap"), new HeapDumpCommand()); - commands.put(Set.of("entity"), new EntityCommand()); - commands.put(Set.of("reload"), new ReloadCommand()); - commands.put(Set.of("version"), new VersionCommand()); - commands.put(Set.of("dumpplugins"), new DumpPluginsCommand()); - commands.put(Set.of("syncloadinfo"), new SyncLoadInfoCommand()); - commands.put(Set.of("dumpitem"), new DumpItemCommand()); - commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand()); - commands.put(Set.of("dumplisteners"), new DumpListenersCommand()); - FeatureHooks.registerPaperCommands(commands); + public static final DateTimeFormatter FILENAME_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss", Locale.ROOT); // could use the formatter in Util too - return commands.entrySet().stream() - .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - }); - private static final Set COMPLETABLE_SUBCOMMANDS = SUBCOMMANDS.entrySet().stream().filter(entry -> entry.getValue().tabCompletes()).map(Map.Entry::getKey).collect(Collectors.toSet()); - // alias -> subcommand label - private static final Map ALIASES = Util.make(() -> { - final Map> aliases = new HashMap<>(); - - aliases.put("version", Set.of("ver")); - - return aliases.entrySet().stream() - .flatMap(entry -> entry.getValue().stream().map(s -> Map.entry(s, entry.getKey()))) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - }); - - public PaperCommand(final String name) { - super(name); - this.description = "Paper related commands"; - this.usageMessage = "/paper [" + String.join(" | ", SUBCOMMANDS.keySet()) + "]"; - final List permissions = new ArrayList<>(); - permissions.add("bukkit.command.paper"); - permissions.addAll(SUBCOMMANDS.keySet().stream().map(s -> BASE_PERM + s).toList()); - this.setPermission(String.join(";", permissions)); - final PluginManager pluginManager = Bukkit.getServer().getPluginManager(); - for (final String perm : permissions) { - pluginManager.addPermission(new Permission(perm, PermissionDefault.OP)); - } + public static Component asFriendlyPath(final Path path) { + return text(path.toString()) + .hoverEvent(text("Click to copy the full path of the file")) + .clickEvent(ClickEvent.copyToClipboard(path.toAbsolutePath().toString())); } - private static boolean testPermission(final CommandSender sender, final String permission) { - if (sender.hasPermission(BASE_PERM + permission) || sender.hasPermission("bukkit.command.paper")) { - return true; - } - sender.sendMessage(Bukkit.permissionMessage()); - return false; - } + public static final String BASE_PERM = "bukkit.command.paper"; + static final String DESCRIPTION = "Paper related commands"; - @Override - public List tabComplete( - final CommandSender sender, - final String alias, - final String[] args, - final @Nullable Location location - ) throws IllegalArgumentException { - if (args.length <= 1) { - return CommandUtil.getListMatchingLast(sender, args, COMPLETABLE_SUBCOMMANDS); - } + public static LiteralCommandNode create() { + final LiteralArgumentBuilder rootNode = Commands.literal("paper") + .requires(source -> source.getSender().hasPermission(BASE_PERM) || SUBPERMISSIONS.stream().anyMatch(name -> source.getSender().hasPermission(name))) + .executes(context -> { + context.getSource().getSender().sendPlainMessage("/paper [" + SUBCOMMANDS.keySet().stream().filter( + name -> hasPermission(name).test(context.getSource()) + ).collect(Collectors.joining(" | ")) + "]"); + return Command.SINGLE_SUCCESS; + }); - final @Nullable Pair subCommand = resolveCommand(args[0]); - if (subCommand != null) { - return subCommand.second().tabComplete(sender, subCommand.first(), Arrays.copyOfRange(args, 1, args.length)); - } + SUBCOMMANDS.values().forEach(rootNode::then); + rootNode.then(VersionCommand.create("ver")); - return Collections.emptyList(); + return rootNode.build(); } - @Override - public boolean execute( - final CommandSender sender, - final String commandLabel, - final String[] args - ) { - if (!testPermission(sender)) { - return true; - } - - if (args.length == 0) { - sender.sendMessage(text("Usage: " + this.usageMessage, RED)); - return false; - } - final @Nullable Pair subCommand = resolveCommand(args[0]); - - if (subCommand == null) { - sender.sendMessage(text("Usage: " + this.usageMessage, RED)); - return false; - } - - if (!testPermission(sender, subCommand.first())) { - return true; - } - final String[] choppedArgs = Arrays.copyOfRange(args, 1, args.length); - return subCommand.second().execute(sender, subCommand.first(), choppedArgs); + public static Predicate hasPermission(String name) { + return source -> source.getSender().hasPermission(BASE_PERM) || source.getSender().hasPermission(BASE_PERM + '.' + name); } - private static @Nullable Pair resolveCommand(String label) { - label = label.toLowerCase(Locale.ROOT); - @Nullable PaperSubcommand subCommand = SUBCOMMANDS.get(label); - if (subCommand == null) { - final @Nullable String command = ALIASES.get(label); - if (command != null) { - label = command; - subCommand = SUBCOMMANDS.get(command); - } - } - - if (subCommand != null) { - return Pair.of(label, subCommand); + public static void registerPermissions() { + final PluginManager pluginManager = Bukkit.getServer().getPluginManager(); + pluginManager.addPermission(new Permission(BASE_PERM, PermissionDefault.OP)); + for (final String permission : SUBPERMISSIONS) { + pluginManager.addPermission(new Permission(permission, PermissionDefault.OP)); } - - return null; } + + private static final Map> SUBCOMMANDS = Util.make(new ObjectArrayList>(), list -> { + list.add(HeapDumpCommand.create()); + list.add(EntityCommand.create()); + list.add(ReloadCommand.create()); + list.add(VersionCommand.create("version")); + list.add(DumpPluginsCommand.create()); + list.add(SyncLoadInfoCommand.create()); + list.add(DumpItemCommand.create()); + list.add(MobcapsCommand.createGlobal()); + list.add(MobcapsCommand.createPlayer()); + list.add(DumpListenersCommand.create()); + FeatureHooks.registerPaperCommands(list); + }).stream().collect(Collectors.toMap(LiteralArgumentBuilder::getLiteral, Function.identity())); + + private static final List SUBPERMISSIONS = SUBCOMMANDS.keySet().stream().map(name -> BASE_PERM + '.' + name).toList(); } diff --git a/paper-server/src/main/java/io/papermc/paper/command/PaperCommands.java b/paper-server/src/main/java/io/papermc/paper/command/PaperCommands.java index 6dfd0b1a85d5..c1a692669bbb 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/PaperCommands.java +++ b/paper-server/src/main/java/io/papermc/paper/command/PaperCommands.java @@ -3,37 +3,36 @@ import com.mojang.brigadier.tree.LiteralCommandNode; import io.papermc.paper.command.brigadier.CommandRegistrationFlag; import io.papermc.paper.command.brigadier.CommandSourceStack; -import net.minecraft.server.MinecraftServer; -import org.bukkit.command.Command; - import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.Util; +import org.bukkit.command.Command; +import org.jspecify.annotations.NullMarked; -@DefaultQualifier(NonNull.class) +@NullMarked public final class PaperCommands { private PaperCommands() { } - private static final Map COMMANDS = new HashMap<>(); - - public static void registerCommands(final MinecraftServer server) { - COMMANDS.put("paper", new PaperCommand("paper")); - COMMANDS.put("mspt", new MSPTCommand("mspt")); + private static final Map COMMANDS = Util.make(new HashMap<>(), map -> { + map.put("mspt", new MSPTCommand("mspt")); + }); + public static void registerLegacyCommands(final MinecraftServer server) { COMMANDS.forEach((s, command) -> { server.server.getCommandMap().register(s, "Paper", command); }); } public static void registerCommands() { - // Paper commands go here + // brigadier commands go here registerInternalCommand(PaperVersionCommand.create(), "bukkit", PaperVersionCommand.DESCRIPTION, List.of("ver", "about"), Set.of()); registerInternalCommand(PaperPluginsCommand.create(), "bukkit", PaperPluginsCommand.DESCRIPTION, List.of("pl"), Set.of()); + registerInternalCommand(PaperCommand.create(), "paper", PaperCommand.DESCRIPTION, List.of(), Set.of()); } private static void registerInternalCommand(final LiteralCommandNode node, final String namespace, final String description, final List aliases, final Set flags) { diff --git a/paper-server/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java b/paper-server/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java index 9c49bb7fb8c3..0562f730181d 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java @@ -34,8 +34,8 @@ import org.jspecify.annotations.NullMarked; @NullMarked -public class PaperPluginsCommand { - public static final String DESCRIPTION = "Gets a list of plugins running on the server"; +public final class PaperPluginsCommand { + static final String DESCRIPTION = "Gets a list of plugins running on the server"; private static final TextColor INFO_COLOR = TextColor.color(52, 159, 218); @@ -67,10 +67,9 @@ public class PaperPluginsCommand { private static final Type JAVA_PLUGIN_PROVIDER_TYPE = new TypeToken>() {}.getType(); public static LiteralCommandNode create() { - final PaperPluginsCommand command = new PaperPluginsCommand(); return Commands.literal("plugins") .requires(source -> source.getSender().hasPermission("bukkit.command.plugins")) - .executes(command::execute) + .executes(PaperPluginsCommand::execute) .build(); } @@ -131,9 +130,9 @@ private static Component header(final String header, final int color, final int } private static Component asPlainComponents(final String strings) { - final net.kyori.adventure.text.TextComponent.Builder builder = Component.text(); + final TextComponent.Builder builder = Component.text(); for (final String string : strings.split("\n")) { - builder.append(Component.newline()); + builder.appendNewline(); builder.append(Component.text(string, NamedTextColor.WHITE)); } @@ -170,7 +169,7 @@ private static TextColor fromStatus(final PluginProvider provider) { } } - private int execute(CommandContext context) { + private static int execute(CommandContext context) { final CommandSender sender = context.getSource().getSender(); final TreeMap> paperPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); final TreeMap> spigotPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); diff --git a/paper-server/src/main/java/io/papermc/paper/command/PaperSubcommand.java b/paper-server/src/main/java/io/papermc/paper/command/PaperSubcommand.java deleted file mode 100644 index 7e9e0ff8639b..000000000000 --- a/paper-server/src/main/java/io/papermc/paper/command/PaperSubcommand.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.papermc.paper.command; - -import java.util.Collections; -import java.util.List; -import org.bukkit.command.CommandSender; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; - -@DefaultQualifier(NonNull.class) -public interface PaperSubcommand { - boolean execute(CommandSender sender, String subCommand, String[] args); - - default List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { - return Collections.emptyList(); - } - - default boolean tabCompletes() { - return true; - } -} diff --git a/paper-server/src/main/java/io/papermc/paper/command/PaperVersionCommand.java b/paper-server/src/main/java/io/papermc/paper/command/PaperVersionCommand.java index bfacb856623b..92849da499af 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/PaperVersionCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/PaperVersionCommand.java @@ -30,8 +30,8 @@ import org.jspecify.annotations.NullMarked; @NullMarked -public class PaperVersionCommand { - public static final String DESCRIPTION = "Gets the version of this server including any plugins in use"; +public final class PaperVersionCommand { + static final String DESCRIPTION = "Gets the version of this server including any plugins in use"; private static final Component NOT_RUNNING = Component.text() .append(Component.text("This server is not running any plugin by that name.")) @@ -45,24 +45,22 @@ public class PaperVersionCommand { private static final Component FAILED_TO_FETCH = Component.text("Could not fetch version information!", NamedTextColor.RED); private static final Component FETCHING = Component.text("Checking version, please wait...", NamedTextColor.WHITE, TextDecoration.ITALIC); - private final VersionFetcher versionFetcher = CraftMagicNumbers.INSTANCE.getVersionFetcher(); - private CompletableFuture computedVersion = CompletableFuture.completedFuture(new ComputedVersion(Component.empty(), -1)); // Precompute-- someday move that stuff out of bukkit + private static final VersionFetcher versionFetcher = CraftMagicNumbers.INSTANCE.getVersionFetcher(); + private static CompletableFuture computedVersion = CompletableFuture.completedFuture(new ComputedVersion(Component.empty(), -1)); // Precompute-- someday move that stuff out of bukkit public static LiteralCommandNode create() { - final PaperVersionCommand command = new PaperVersionCommand(); - return Commands.literal("version") .requires(source -> source.getSender().hasPermission("bukkit.command.version")) + .executes(PaperVersionCommand::serverVersion) .then(Commands.argument("plugin", StringArgumentType.word()) - .suggests(command::suggestPlugins) - .executes(command::pluginVersion)) - .executes(command::serverVersion) + .suggests(PaperVersionCommand::suggestPlugins) + .executes(PaperVersionCommand::pluginVersion)) .build(); } - private int pluginVersion(final CommandContext context) { + private static int pluginVersion(final CommandContext context) { final CommandSender sender = context.getSource().getSender(); - final String pluginName = context.getArgument("plugin", String.class).toLowerCase(Locale.ROOT); + final String pluginName = StringArgumentType.getString(context, "plugin").toLowerCase(Locale.ROOT); Plugin plugin = Bukkit.getPluginManager().getPlugin(pluginName); if (plugin == null) { @@ -73,18 +71,18 @@ private int pluginVersion(final CommandContext context) { } if (plugin != null) { - this.sendPluginInfo(plugin, sender); + sendPluginInfo(plugin, sender); + return Command.SINGLE_SUCCESS; } else { sender.sendMessage(NOT_RUNNING); + return 0; } - - return Command.SINGLE_SUCCESS; } - private CompletableFuture suggestPlugins(final CommandContext context, final SuggestionsBuilder builder) { + private static CompletableFuture suggestPlugins(final CommandContext context, final SuggestionsBuilder builder) { for (final Plugin plugin : Bukkit.getPluginManager().getPlugins()) { final String name = plugin.getName(); - if (StringUtil.startsWithIgnoreCase(name, builder.getRemainingLowerCase())) { + if (StringUtil.startsWithIgnoreCase(name, builder.getRemaining())) { builder.suggest(name); } } @@ -92,7 +90,7 @@ private CompletableFuture suggestPlugins(final CommandContext names) { return Component.join(PLAYER_JOIN_CONFIGURATION, names.stream().map(Component::text).toList()).color(NamedTextColor.GREEN); } - private int serverVersion(CommandContext context) { + public static int serverVersion(CommandContext context) { sendVersion(context.getSource().getSender()); return Command.SINGLE_SUCCESS; } - private void sendVersion(final CommandSender sender) { + private static void sendVersion(final CommandSender sender) { final CompletableFuture version = getVersionOrFetch(); if (!version.isDone()) { sender.sendMessage(FETCHING); @@ -150,24 +148,24 @@ private void sendVersion(final CommandSender sender) { }); } - private CompletableFuture getVersionOrFetch() { - if (!this.computedVersion.isDone()) { - return this.computedVersion; + private static CompletableFuture getVersionOrFetch() { + if (!computedVersion.isDone()) { + return computedVersion; } - if (this.computedVersion.isCompletedExceptionally() || System.currentTimeMillis() - this.computedVersion.resultNow().computedTime() > this.versionFetcher.getCacheTime()) { - this.computedVersion = this.fetchVersionMessage(); + if (computedVersion.isCompletedExceptionally() || System.currentTimeMillis() - computedVersion.resultNow().computedTime() > versionFetcher.getCacheTime()) { + computedVersion = fetchVersionMessage(); } - return this.computedVersion; + return computedVersion; } - private CompletableFuture fetchVersionMessage() { + private static CompletableFuture fetchVersionMessage() { return CompletableFuture.supplyAsync(() -> { final Component message = Component.textOfChildren( Component.text(Bukkit.getVersionMessage(), NamedTextColor.WHITE), Component.newline(), - this.versionFetcher.getVersionMessage() + versionFetcher.getVersionMessage() ); return new ComputedVersion( 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 index bff395667f69..9e1f1ecacb2a 100644 --- 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 @@ -61,12 +61,13 @@ import net.minecraft.commands.arguments.GameProfileArgument; import net.minecraft.commands.arguments.HeightmapTypeArgument; import net.minecraft.commands.arguments.HexColorArgument; +import net.minecraft.commands.arguments.IdentifierArgument; 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.IdentifierArgument; +import net.minecraft.commands.arguments.ResourceOrTagArgument; import net.minecraft.commands.arguments.ScoreboardSlotArgument; import net.minecraft.commands.arguments.StyleArgument; import net.minecraft.commands.arguments.TemplateMirrorArgument; @@ -85,6 +86,8 @@ import net.minecraft.commands.arguments.item.ItemPredicateArgument; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; @@ -407,7 +410,17 @@ public ArgumentType templateRotation() { public ArgumentType> resourceKey(final RegistryKey registryKey) { return this.wrap( ResourceKeyArgument.key(PaperRegistries.registryToNms(registryKey)), - nmsRegistryKey -> TypedKey.create(registryKey, CraftNamespacedKey.fromMinecraft(nmsRegistryKey.identifier())) + nmsRegistryKey -> TypedKey.create(registryKey, PaperAdventure.asAdventure(nmsRegistryKey.identifier())) + ); + } + + // expose to api? + public static ArgumentType>> resourceOrTag(final ResourceKey> registryKey) { + return new NativeWrapperArgumentType<>( + ResourceOrTagArgument.resourceOrTag(PaperCommands.INSTANCE.getBuildContext(), registryKey), + result -> result.unwrap().map(List::of, tag -> { + return tag.stream().toList(); + }) ); } diff --git a/paper-server/src/main/java/io/papermc/paper/command/brigadier/exception/DynamicCommandExceptionType.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/exception/DynamicCommandExceptionType.java new file mode 100644 index 000000000000..316a452b72c8 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/command/brigadier/exception/DynamicCommandExceptionType.java @@ -0,0 +1,26 @@ +package io.papermc.paper.command.brigadier.exception; + +import com.mojang.brigadier.ImmutableStringReader; +import com.mojang.brigadier.Message; +import com.mojang.brigadier.exceptions.CommandExceptionType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import java.util.function.Function; +import io.papermc.paper.command.brigadier.MessageComponentSerializer; +import net.kyori.adventure.text.Component; + +public class DynamicCommandExceptionType implements CommandExceptionType { + + private final Function function; + + public DynamicCommandExceptionType(final Function function) { + this.function = arg -> MessageComponentSerializer.message().serialize(function.apply(arg)); + } + + public CommandSyntaxException create(final Object arg) { + return new CommandSyntaxException(this, this.function.apply(arg)); + } + + public CommandSyntaxException createWithContext(final ImmutableStringReader reader, final Object arg) { + return new CommandSyntaxException(this, this.function.apply(arg), reader.getString(), reader.getCursor()); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/command/brigadier/exception/SimpleCommandExceptionType.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/exception/SimpleCommandExceptionType.java new file mode 100644 index 000000000000..01e189840066 --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/command/brigadier/exception/SimpleCommandExceptionType.java @@ -0,0 +1,25 @@ +package io.papermc.paper.command.brigadier.exception; + +import com.mojang.brigadier.ImmutableStringReader; +import com.mojang.brigadier.Message; +import com.mojang.brigadier.exceptions.CommandExceptionType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import io.papermc.paper.command.brigadier.MessageComponentSerializer; +import net.kyori.adventure.text.Component; + +public class SimpleCommandExceptionType implements CommandExceptionType { + + private final Message message; + + public SimpleCommandExceptionType(final Component message) { + this.message = MessageComponentSerializer.message().serialize(message); + } + + public CommandSyntaxException create() { + return new CommandSyntaxException(this, this.message); + } + + public CommandSyntaxException createWithContext(final ImmutableStringReader reader) { + return new CommandSyntaxException(this, this.message, reader.getString(), reader.getCursor()); + } +} diff --git a/paper-server/src/main/java/io/papermc/paper/command/subcommands/DumpItemCommand.java b/paper-server/src/main/java/io/papermc/paper/command/subcommands/DumpItemCommand.java index 4c3d5712c677..8ab8c25f9c79 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/subcommands/DumpItemCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/subcommands/DumpItemCommand.java @@ -1,8 +1,11 @@ package io.papermc.paper.command.subcommands; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; import io.papermc.paper.adventure.PaperAdventure; -import io.papermc.paper.command.CommandUtil; -import io.papermc.paper.command.PaperSubcommand; +import io.papermc.paper.command.PaperCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; import java.util.ArrayList; import java.util.Collections; import java.util.IdentityHashMap; @@ -15,13 +18,11 @@ import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.TextComponent; -import net.minecraft.core.Registry; -import net.minecraft.core.RegistryAccess; import net.minecraft.core.component.DataComponentMap; import net.minecraft.core.component.DataComponentPatch; import net.minecraft.core.component.DataComponentType; import net.minecraft.core.component.TypedDataComponent; -import net.minecraft.core.registries.Registries; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.NbtUtils; import net.minecraft.nbt.SnbtPrinterTagVisitor; @@ -29,13 +30,11 @@ import net.minecraft.resources.RegistryOps; import net.minecraft.world.item.ItemStack; import org.bukkit.command.CommandSender; -import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftRegistry; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.entity.Player; -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; import static net.kyori.adventure.text.Component.join; import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.Component.textOfChildren; @@ -48,22 +47,33 @@ import static net.kyori.adventure.text.format.TextColor.color; import static net.kyori.adventure.text.format.TextDecoration.ITALIC; -@DefaultQualifier(NonNull.class) -public final class DumpItemCommand implements PaperSubcommand { - @Override - public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { - this.doDumpItem(sender, args.length > 0 && "all".equals(args[0])); - return true; +public final class DumpItemCommand { + + public static LiteralArgumentBuilder create() { + return Commands.literal("dumpitem") + .requires(PaperCommand.hasPermission("dumpitem")) + .executes(context -> { + if (!(context.getSource().getExecutor() instanceof Player player)) { + throw net.minecraft.commands.CommandSourceStack.ERROR_NOT_PLAYER.create(); + } + doDumpItem(player, false, context.getSource().getSender()); + return Command.SINGLE_SUCCESS; + }) + .then(Commands.literal("all") + .executes(context -> { + if (!(context.getSource().getExecutor() instanceof Player player)) { + throw net.minecraft.commands.CommandSourceStack.ERROR_NOT_PLAYER.create(); + } + doDumpItem(player, true, context.getSource().getSender()); + return Command.SINGLE_SUCCESS; + }) + ); } @SuppressWarnings({"unchecked", "OptionalAssignedToNull", "rawtypes"}) - private void doDumpItem(final CommandSender sender, final boolean includeAllComponents) { - if (!(sender instanceof final Player player)) { - sender.sendMessage("Only players can use this command"); - return; - } + private static void doDumpItem(final Player player, final boolean includeAllComponents, final CommandSender sender) { final ItemStack itemStack = CraftItemStack.asNMSCopy(player.getInventory().getItemInMainHand()); - final TextComponent.Builder visualOutput = Component.text(); + final TextComponent.Builder visualOutput = text(); final StringBuilder itemCommandBuilder = new StringBuilder(); final String itemName = itemStack.getItemHolder().unwrapKey().orElseThrow().identifier().toString(); itemCommandBuilder.append(itemName); @@ -76,15 +86,13 @@ private void doDumpItem(final CommandSender sender, final boolean includeAllComp referencedComponentTypes.addAll(prototype.keySet()); } - final RegistryAccess.Frozen access = ((CraftServer) sender.getServer()).getServer().registryAccess(); - final RegistryOps ops = access.createSerializationContext(NbtOps.INSTANCE); - final Registry> registry = access.lookupOrThrow(Registries.DATA_COMPONENT_TYPE); + final RegistryOps ops = CraftRegistry.getMinecraftRegistry().createSerializationContext(NbtOps.INSTANCE); final List componentComponents = new ArrayList<>(); final List commandComponents = new ArrayList<>(); for (final DataComponentType type : referencedComponentTypes) { - final String path = registry.getResourceKey(type).orElseThrow().identifier().getPath(); - final @Nullable Optional patchedValue = patch.get(type); - final @Nullable TypedDataComponent prototypeValue = prototype.getTyped(type); + final String path = requireNonNull(BuiltInRegistries.DATA_COMPONENT_TYPE.getKey(type)).getPath(); + final Optional patchedValue = patch.get(type); + final TypedDataComponent prototypeValue = prototype.getTyped(type); if (patchedValue != null) { if (patchedValue.isEmpty()) { componentComponents.add(text().append(text('!', RED), text(path, AQUA))); @@ -109,7 +117,7 @@ private void doDumpItem(final CommandSender sender, final boolean includeAllComp .append(String.join(",", commandComponents)) .append("]"); } - player.sendMessage(visualOutput.build().compact()); + sender.sendMessage(visualOutput.build().compact()); final Component copyMsg = text("Click to copy item definition to clipboard for use with /give", GRAY, ITALIC); sender.sendMessage(copyMsg.clickEvent(copyToClipboard(itemCommandBuilder.toString()))); } @@ -122,12 +130,4 @@ private static void writeComponentValue(final Consumer visualOutput, )); commandOutput.accept(path + "=" + new SnbtPrinterTagVisitor("", 0, new ArrayList<>()).visit(serialized)); } - - @Override - public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { - if (args.length == 1) { - return CommandUtil.getListMatchingLast(sender, args, "all"); - } - return Collections.emptyList(); - } } diff --git a/paper-server/src/main/java/io/papermc/paper/command/subcommands/DumpListenersCommand.java b/paper-server/src/main/java/io/papermc/paper/command/subcommands/DumpListenersCommand.java index 9c79d8757f14..2cddf5e8d818 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/subcommands/DumpListenersCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/subcommands/DumpListenersCommand.java @@ -1,148 +1,141 @@ package io.papermc.paper.command.subcommands; import com.destroystokyo.paper.util.SneakyThrow; -import io.papermc.paper.command.PaperSubcommand; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import io.papermc.paper.command.PaperCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; import java.io.IOException; import java.io.PrintWriter; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; -import java.lang.reflect.Field; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Set; +import java.util.Map; +import java.util.concurrent.CompletableFuture; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.event.ClickEvent; import net.minecraft.server.MinecraftServer; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.event.HandlerList; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.RegisteredListener; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.framework.qual.DefaultQualifier; +import org.bukkit.util.StringUtil; -import static net.kyori.adventure.text.Component.newline; -import static net.kyori.adventure.text.Component.space; import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.format.NamedTextColor.GRAY; import static net.kyori.adventure.text.format.NamedTextColor.GREEN; import static net.kyori.adventure.text.format.NamedTextColor.RED; -import static net.kyori.adventure.text.format.NamedTextColor.WHITE; -@DefaultQualifier(NonNull.class) -public final class DumpListenersCommand implements PaperSubcommand { - private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"); +public final class DumpListenersCommand { + private static final String COMMAND_ARGUMENT_TO_FILE = "tofile"; - private static final MethodHandle EVENT_TYPES_HANDLE; + private static final Component HELP_MESSAGE = text("Usage: /paper dumplisteners " + COMMAND_ARGUMENT_TO_FILE + "|", RED); + private static final SuggestionProvider EVENT_SUGGESTIONS = (context, builder) -> CompletableFuture.supplyAsync(() -> { + eventClassNames().entrySet().stream() + .filter(entry -> { + if (StringUtil.startsWithIgnoreCase(entry.getKey(), builder.getRemaining()) || StringUtil.startsWithIgnoreCase(entry.getKey().substring(entry.getKey().lastIndexOf('.') + 1), builder.getRemaining())) { + return entry.getValue().getRegisteredListeners().length != 0; + } + return false; + }) + .map(Map.Entry::getKey) + .forEach(builder::suggest); + return builder.build(); + }); + + private static final MethodHandle EVENT_TYPES_HANDLE; static { try { - final Field eventTypesField = HandlerList.class.getDeclaredField("EVENT_TYPES"); - eventTypesField.setAccessible(true); - EVENT_TYPES_HANDLE = MethodHandles.lookup().unreflectGetter(eventTypesField); + MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(HandlerList.class, MethodHandles.lookup()); + EVENT_TYPES_HANDLE = lookup.findStaticGetter(HandlerList.class, "EVENT_TYPES", Map.class); } catch (final ReflectiveOperationException e) { throw new RuntimeException(e); } } - @Override - public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { - if (args.length >= 1 && args[0].equals(COMMAND_ARGUMENT_TO_FILE)) { - this.dumpToFile(sender); - return true; - } - this.doDumpListeners(sender, args); - return true; + public static LiteralArgumentBuilder create() { + return Commands.literal("dumplisteners") + .requires(PaperCommand.hasPermission("dumplisteners")) + .executes(context -> { + context.getSource().getSender().sendMessage(HELP_MESSAGE); + return Command.SINGLE_SUCCESS; + }) + .then(Commands.literal(COMMAND_ARGUMENT_TO_FILE) + .executes(context -> { + return dumpToFile(context.getSource().getSender()); + }) + ) + .then(Commands.argument("event", StringArgumentType.word()) + .suggests(EVENT_SUGGESTIONS) + .executes(context -> { + return doDumpListeners(context.getSource().getSender(), StringArgumentType.getString(context, "event")); + }) + ); } - private void dumpToFile(final CommandSender sender) { - Path parent = Path.of("debug"); - Path path = parent.resolve("listeners-" + FORMATTER.format(LocalDateTime.now()) + ".txt"); + private static int dumpToFile(final CommandSender sender) { + Path path = Path.of("debug", "listeners-" + PaperCommand.FILENAME_DATE_TIME_FORMATTER.format(LocalDateTime.now()) + ".txt"); sender.sendMessage( - text("Writing listeners into directory", GREEN) + text("Writing listeners into", GREEN) .appendSpace() - .append( - text(parent.toString(), WHITE) - .hoverEvent(text("Click to copy the full path of debug directory", WHITE)) - .clickEvent(ClickEvent.copyToClipboard(parent.toAbsolutePath().toString())) - ) + .append(PaperCommand.asFriendlyPath(path)) ); try { - Files.createDirectories(parent); - Files.createFile(path); - try (final PrintWriter writer = new PrintWriter(path.toFile())){ - for (final String eventClass : eventClassNames()) { - final HandlerList handlers; - try { - handlers = (HandlerList) findClass(eventClass).getMethod("getHandlerList").invoke(null); - } catch (final ReflectiveOperationException e) { - continue; - } + Files.createDirectories(path.getParent()); + try (final PrintWriter writer = new PrintWriter(path.toFile())) { + for (final Map.Entry entry : eventClassNames().entrySet()) { + final HandlerList handlers = entry.getValue(); if (handlers.getRegisteredListeners().length != 0) { - writer.println(eventClass); + writer.println(entry.getKey()); } for (final RegisteredListener registeredListener : handlers.getRegisteredListeners()) { writer.println(" - " + registeredListener); } } } + sender.sendMessage(text("Successfully written listeners", GREEN)); + return Command.SINGLE_SUCCESS; } catch (final IOException ex) { sender.sendMessage(text("Failed to write dumped listener! See the console for more info.", RED)); MinecraftServer.LOGGER.warn("Error occurred while dumping listeners", ex); - return; + return 0; } - sender.sendMessage( - text("Successfully written listeners into", GREEN) - .appendSpace() - .append( - text(path.toString(), WHITE) - .hoverEvent(text("Click to copy the full path of the file", WHITE)) - .clickEvent(ClickEvent.copyToClipboard(path.toAbsolutePath().toString())) - ) - ); } - private void doDumpListeners(final CommandSender sender, final String[] args) { - if (args.length == 0) { - sender.sendMessage(text("Usage: /paper dumplisteners " + COMMAND_ARGUMENT_TO_FILE + "|", RED)); - return; - } - - final String className = args[0]; - + private static int doDumpListeners(final CommandSender sender, final String className) { try { final HandlerList handlers = (HandlerList) findClass(className).getMethod("getHandlerList").invoke(null); if (handlers.getRegisteredListeners().length == 0) { - sender.sendMessage(text(className + " does not have any registered listeners.")); - return; + sender.sendPlainMessage(className + " does not have any registered listeners."); + return 0; } - sender.sendMessage(text("Listeners for " + className + ":")); + sender.sendPlainMessage("Listeners for " + className + ":"); for (final RegisteredListener listener : handlers.getRegisteredListeners()) { - final Component hoverText = text("Priority: " + listener.getPriority().name() + " (" + listener.getPriority().getSlot() + ")", WHITE) - .append(newline()) + final Component hoverText = text("Priority: " + listener.getPriority().name() + " (" + listener.getPriority().getSlot() + ")") + .appendNewline() .append(text("Listener: " + listener.getListener())) - .append(newline()) + .appendNewline() .append(text("Executor: " + listener.getExecutor())) - .append(newline()) + .appendNewline() .append(text("Ignoring cancelled: " + listener.isIgnoringCancelled())); sender.sendMessage(text(listener.getPlugin().getName(), GREEN) - .append(space()) + .appendSpace() .append(text("(" + listener.getListener().getClass().getName() + ")", GRAY).hoverEvent(hoverText))); } - sender.sendMessage(text("Total listeners: " + handlers.getRegisteredListeners().length)); - + int size = handlers.getRegisteredListeners().length; + sender.sendPlainMessage("Total listeners: " + size); + return size; } catch (final ClassNotFoundException e) { sender.sendMessage(text("Unable to find a class named '" + className + "'. Make sure to use the fully qualified name.", RED)); } catch (final NoSuchMethodException e) { @@ -151,40 +144,23 @@ private void doDumpListeners(final CommandSender sender, final String[] args) { sender.sendMessage(text("Something went wrong, see the console for more details.", RED)); MinecraftServer.LOGGER.warn("Error occurred while dumping listeners for class {}", className, e); } - } - - @Override - public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { - return switch (args.length) { - case 0 -> suggestions(); - case 1 -> suggestions().stream() - .filter(clazz -> clazz.toLowerCase(Locale.ROOT).contains(args[0].toLowerCase(Locale.ROOT))) - .toList(); - default -> Collections.emptyList(); - }; - } - - private static List suggestions() { - final List ret = new ArrayList<>(); - ret.add(COMMAND_ARGUMENT_TO_FILE); - ret.addAll(eventClassNames()); - return ret; + return 0; } @SuppressWarnings("unchecked") - private static Set eventClassNames() { + private static Map eventClassNames() { try { - return (Set) EVENT_TYPES_HANDLE.invokeExact(); + return (Map) EVENT_TYPES_HANDLE.invokeExact(); } catch (final Throwable e) { SneakyThrow.sneaky(e); - return Collections.emptySet(); // Unreachable + return Map.of(); // Unreachable } } private static Class findClass(final String className) throws ClassNotFoundException { try { return Class.forName(className); - } catch (final ClassNotFoundException ignore) { + } catch (final ClassNotFoundException ignored) { for (final Plugin plugin : Bukkit.getServer().getPluginManager().getPlugins()) { if (!plugin.isEnabled()) { continue; diff --git a/paper-server/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java b/paper-server/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java index 0f8983d07eb1..35feec976ebb 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java @@ -8,7 +8,11 @@ import com.google.gson.Strictness; import com.google.gson.internal.Streams; import com.google.gson.stream.JsonWriter; -import io.papermc.paper.command.PaperSubcommand; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import io.papermc.paper.command.PaperCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; import io.papermc.paper.plugin.entrypoint.Entrypoint; import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; import io.papermc.paper.plugin.entrypoint.classloader.group.LockingClassLoaderGroup; @@ -17,67 +21,54 @@ import io.papermc.paper.plugin.entrypoint.classloader.group.SpigotPluginClassLoaderGroup; import io.papermc.paper.plugin.entrypoint.classloader.group.StaticPluginClassLoaderGroup; import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree; -import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; -import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy; import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration; +import io.papermc.paper.plugin.entrypoint.strategy.modern.ModernPluginLoadingStrategy; import io.papermc.paper.plugin.manager.PaperPluginManagerImpl; import io.papermc.paper.plugin.provider.PluginProvider; import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage; import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; import io.papermc.paper.plugin.storage.ConfiguredProviderStorage; import io.papermc.paper.plugin.storage.ProviderStorage; -import net.kyori.adventure.text.event.ClickEvent; -import net.minecraft.server.MinecraftServer; -import org.bukkit.command.CommandSender; -import org.bukkit.plugin.Plugin; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; - -import java.io.IOException; import java.io.PrintStream; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Map; +import net.minecraft.server.MinecraftServer; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.format.NamedTextColor.GREEN; import static net.kyori.adventure.text.format.NamedTextColor.RED; -import static net.kyori.adventure.text.format.NamedTextColor.WHITE; - -@DefaultQualifier(NonNull.class) -public final class DumpPluginsCommand implements PaperSubcommand { - @Override - public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { - this.dumpPlugins(sender, args); - return true; - } - private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"); +public final class DumpPluginsCommand { - private void dumpPlugins(final CommandSender sender, final String[] args) { - Path parent = Path.of("debug"); - Path path = parent.resolve("plugin-info-" + FORMATTER.format(LocalDateTime.now()) + ".json"); + public static LiteralArgumentBuilder create() { + return Commands.literal("dumpplugins") + .requires(PaperCommand.hasPermission("dumpplugins")) + .executes(context -> { + return dumpPlugins(context.getSource().getSender()); + }); + } + + private static int dumpPlugins(final CommandSender sender) { + Path path = Path.of("debug", "plugin-info-" + PaperCommand.FILENAME_DATE_TIME_FORMATTER.format(LocalDateTime.now()) + ".json"); try { - Files.createDirectories(parent); - Files.createFile(path); + Files.createDirectories(path.getParent()); sender.sendMessage( - text("Writing plugin information into directory", GREEN) + text("Writing plugin debug information into", GREEN) .appendSpace() - .append( - text(parent.toString(), WHITE) - .hoverEvent(text("Click to copy the full path of debug directory", WHITE)) - .clickEvent(ClickEvent.copyToClipboard(parent.toAbsolutePath().toString())) - ) + .append(PaperCommand.asFriendlyPath(path)) ); - final JsonObject data = this.writeDebug(); + final JsonObject data = writeDebug(); StringWriter stringWriter = new StringWriter(); JsonWriter jsonWriter = new JsonWriter(stringWriter); @@ -88,36 +79,30 @@ private void dumpPlugins(final CommandSender sender, final String[] args) { try (PrintStream out = new PrintStream(Files.newOutputStream(path), false, StandardCharsets.UTF_8)) { out.print(stringWriter); } - sender.sendMessage( - text("Successfully written plugin debug information into", GREEN) - .appendSpace() - .append( - text(path.toString(), WHITE) - .hoverEvent(text("Click to copy the full path of the file", WHITE)) - .clickEvent(ClickEvent.copyToClipboard(path.toAbsolutePath().toString())) - ) - ); - } catch (Throwable e) { + sender.sendMessage(text("Successfully written plugin debug information", GREEN)); + return Command.SINGLE_SUCCESS; + } catch (Throwable thr) { sender.sendMessage(text("Failed to write plugin information! See the console for more info.", RED)); - MinecraftServer.LOGGER.warn("Error occurred while dumping plugin info", e); + MinecraftServer.LOGGER.warn("Error occurred while dumping plugin info", thr); + return 0; } } - private JsonObject writeDebug() { + private static JsonObject writeDebug() { JsonObject root = new JsonObject(); if (ConfiguredProviderStorage.LEGACY_PLUGIN_LOADING) { root.addProperty("legacy-loading-strategy", true); } - this.writeProviders(root); - this.writePlugins(root); - this.writeClassloaders(root); + writeProviders(root); + writePlugins(root); + writeClassloaders(root); return root; } @SuppressWarnings("unchecked") - private void writeProviders(JsonObject root) { + private static void writeProviders(JsonObject root) { JsonObject rootProviders = new JsonObject(); root.add("providers", rootProviders); @@ -166,7 +151,7 @@ public boolean preloadProvider(PluginProvider provider) { } } - private void writePlugins(JsonObject root) { + private static void writePlugins(JsonObject root) { JsonArray rootPlugins = new JsonArray(); root.add("plugins", rootPlugins); @@ -175,7 +160,7 @@ private void writePlugins(JsonObject root) { } } - private void writeClassloaders(JsonObject root) { + private static void writeClassloaders(JsonObject root) { JsonObject classLoadersRoot = new JsonObject(); root.add("classloaders", classLoadersRoot); @@ -186,11 +171,11 @@ private void writeClassloaders(JsonObject root) { JsonArray array = new JsonArray(); classLoadersRoot.add("children", array); for (PluginClassLoaderGroup group : storage.getGroups()) { - array.add(this.writeClassloader(group)); + array.add(writeClassloader(group)); } } - private JsonObject writeClassloader(PluginClassLoaderGroup group) { + private static JsonObject writeClassloader(PluginClassLoaderGroup group) { JsonObject classLoadersRoot = new JsonObject(); if (group instanceof SimpleListPluginClassLoaderGroup listGroup) { JsonArray array = new JsonArray(); @@ -203,12 +188,12 @@ private JsonObject writeClassloader(PluginClassLoaderGroup group) { classLoadersRoot.add("children", array); for (ConfiguredPluginClassLoader innerGroup : listGroup.getClassLoaders()) { - array.add(this.writeClassloader(innerGroup)); + array.add(writeClassloader(innerGroup)); } } else if (group instanceof LockingClassLoaderGroup locking) { // Unwrap - return this.writeClassloader(locking.getParent()); + return writeClassloader(locking.getParent()); } else { classLoadersRoot.addProperty("raw", group.toString()); } @@ -216,7 +201,7 @@ private JsonObject writeClassloader(PluginClassLoaderGroup group) { return classLoadersRoot; } - private JsonElement writeClassloader(ConfiguredPluginClassLoader innerGroup) { + private static JsonElement writeClassloader(ConfiguredPluginClassLoader innerGroup) { return new JsonPrimitive(innerGroup.toString()); } } diff --git a/paper-server/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java b/paper-server/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java index e3e21acb82b5..73bc891c201f 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/subcommands/EntityCommand.java @@ -1,19 +1,21 @@ package io.papermc.paper.command.subcommands; -import com.google.common.collect.Maps; -import io.papermc.paper.FeatureHooks; -import io.papermc.paper.command.CommandUtil; -import io.papermc.paper.command.PaperSubcommand; -import java.util.Collections; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import io.papermc.paper.command.PaperCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import io.papermc.paper.command.brigadier.argument.VanillaArgumentProviderImpl; +import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickEvent; -import net.kyori.adventure.text.event.HoverEvent; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.core.Holder; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.resources.Identifier; import net.minecraft.server.level.ServerChunkCache; import net.minecraft.server.level.ServerLevel; @@ -21,138 +23,105 @@ import net.minecraft.world.level.ChunkPos; import org.apache.commons.lang3.tuple.MutablePair; import org.apache.commons.lang3.tuple.Pair; -import org.bukkit.Bukkit; -import org.bukkit.HeightMap; import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.craftbukkit.CraftWorld; -import org.bukkit.entity.Player; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.framework.qual.DefaultQualifier; import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.format.NamedTextColor.GREEN; -import static net.kyori.adventure.text.format.NamedTextColor.RED; -@DefaultQualifier(NonNull.class) -public final class EntityCommand implements PaperSubcommand { - @Override - public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { - this.listEntities(sender, args); - return true; - } +public final class EntityCommand { - @Override - public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { - if (args.length == 1) { - return CommandUtil.getListMatchingLast(sender, args, "help", "list"); - } else if (args.length == 2) { - return CommandUtil.getListMatchingLast(sender, args, BuiltInRegistries.ENTITY_TYPE.keySet()); - } - return Collections.emptyList(); + public static LiteralArgumentBuilder create() { + return Commands.literal("entity") + .requires(PaperCommand.hasPermission("entity")) + .then(Commands.literal("list") + .executes(context -> { + return listEntities(context.getSource().getSender(), BuiltInRegistries.ENTITY_TYPE.listElements().toList(), context.getSource().getLocation().getWorld()); + }) + .then(Commands.argument("entity-type", VanillaArgumentProviderImpl.resourceOrTag(Registries.ENTITY_TYPE)) + .executes(context -> { + return listEntities( + context.getSource().getSender(), + (List>>) context.getArgument("entity-type", List.class), + context.getSource().getLocation().getWorld() + ); + }) + .then(Commands.argument("world", ArgumentTypes.world()) + .executes(context -> { + return listEntities( + context.getSource().getSender(), + (List>>) context.getArgument("entity-type", List.class), + context.getArgument("world", World.class) + ); + }) + ) + ) + ); } /* * Ported from MinecraftForge - author: LexManos - License: LGPLv2.1 */ - private void listEntities(final CommandSender sender, final String[] args) { - // help - if (args.length < 1 || !args[0].toLowerCase(Locale.ROOT).equals("list")) { - sender.sendMessage(text("Use /paper entity [list] help for more information on a specific command", RED)); - return; - } + private static int listEntities(final CommandSender sender, final List>> entities, final World bukkitWorld) throws CommandSyntaxException { + Map>> list = new HashMap<>(); + ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); + Map nonEntityTicking = new HashMap<>(); + ServerChunkCache chunkProviderServer = world.getChunkSource(); + world.getAllEntities().forEach(e -> { + Identifier key = EntityType.getKey(e.getType()); - if ("list".equals(args[0].toLowerCase(Locale.ROOT))) { - String filter = "*"; - if (args.length > 1) { - if (args[1].toLowerCase(Locale.ROOT).equals("help")) { - sender.sendMessage(text("Use /paper entity list [filter] [worldName] to get entity info that matches the optional filter.", RED)); - return; - } - filter = args[1]; + MutablePair> info = list.computeIfAbsent(key, k -> MutablePair.of(0, new HashMap<>())); + ChunkPos chunk = e.chunkPosition(); + info.left++; + info.right.put(chunk, info.right.getOrDefault(chunk, 0) + 1); + if (!world.isPositionEntityTicking(e.blockPosition()) || (e instanceof net.minecraft.world.entity.Marker && !world.paperConfig().entities.markers.tick)) { // Paper - Configurable marker ticking + nonEntityTicking.merge(key, 1, Integer::sum); } - final String cleanfilter = filter.replace("?", ".?").replace("*", ".*?"); - Set names = BuiltInRegistries.ENTITY_TYPE.keySet().stream() - .filter(n -> n.toString().matches(cleanfilter)) - .collect(Collectors.toSet()); - if (names.isEmpty()) { - sender.sendMessage(text("Invalid filter, does not match any entities. Use /paper entity list for a proper list", RED)); - sender.sendMessage(text("Usage: /paper entity list [filter] [worldName]", RED)); - return; + }); + if (entities.size() == 1) { + Identifier name = entities.getFirst().unwrapKey().orElseThrow().identifier(); + Pair> info = list.get(name); + int nonTicking = nonEntityTicking.getOrDefault(name, 0); + if (info == null) { + throw EntityArgument.NO_ENTITIES_FOUND.create(); } - String worldName; - if (args.length > 2) { - worldName = args[2]; - } else if (sender instanceof Player) { - worldName = ((Player) sender).getWorld().getName(); - } else { - sender.sendMessage(text("Please specify the name of a world", RED)); - sender.sendMessage(text("To do so without a filter, specify '*' as the filter", RED)); - sender.sendMessage(text("Usage: /paper entity list [filter] [worldName]", RED)); - return; - } - Map>> list = Maps.newHashMap(); - @Nullable World bukkitWorld = Bukkit.getWorld(worldName); - if (bukkitWorld == null) { - sender.sendMessage(text("Could not load world for " + worldName + ". Please select a valid world.", RED)); - sender.sendMessage(text("Usage: /paper entity list [filter] [worldName]", RED)); - return; + sender.sendPlainMessage("Entity: " + name + " Total Ticking: " + (info.getLeft() - nonTicking) + ", Total Non-Ticking: " + nonTicking); + List> entitiesPerChunk = info.getRight().entrySet().stream() + .sorted((a, b) -> !a.getValue().equals(b.getValue()) ? b.getValue() - a.getValue() : a.getKey().toString().compareTo(b.getKey().toString())) + .limit(10).toList(); + entitiesPerChunk.forEach(e -> { + final int x = (e.getKey().x << 4) + 8; + final int z = (e.getKey().z << 4) + 8; + final Component message = text(" " + e.getValue() + ": " + e.getKey().x + ", " + e.getKey().z + (chunkProviderServer.isPositionTicking(e.getKey().toLong()) ? " (Ticking)" : " (Non-Ticking)")) + .hoverEvent(text("Click to teleport to chunk", GREEN)) + .clickEvent(ClickEvent.runCommand("/minecraft:execute in " + world.getWorld().getKey() + " run tp " + x + " ~ " + z)); + sender.sendMessage(message); + }); + return entitiesPerChunk.size(); + } else { + List names = entities.stream().map(type -> type.unwrapKey().orElseThrow().identifier()).toList(); + List> info = list.entrySet().stream() + .filter(e -> names.contains(e.getKey())) + .map(e -> Pair.of(e.getKey(), e.getValue().left)) + .sorted((a, b) -> !a.getRight().equals(b.getRight()) ? b.getRight() - a.getRight() : a.getKey().toString().compareTo(b.getKey().toString())) + .toList(); + + if (info.isEmpty()) { + throw EntityArgument.NO_ENTITIES_FOUND.create(); } - ServerLevel world = ((CraftWorld) bukkitWorld).getHandle(); - Map nonEntityTicking = Maps.newHashMap(); - ServerChunkCache chunkProviderServer = world.getChunkSource(); - FeatureHooks.getAllEntities(world).forEach(e -> { - Identifier key = EntityType.getKey(e.getType()); - MutablePair> info = list.computeIfAbsent(key, k -> MutablePair.of(0, Maps.newHashMap())); - ChunkPos chunk = e.chunkPosition(); - info.left++; - info.right.put(chunk, info.right.getOrDefault(chunk, 0) + 1); - if (!world.isPositionEntityTicking(e.blockPosition()) || (e instanceof net.minecraft.world.entity.Marker && !world.paperConfig().entities.markers.tick)) { // Paper - Configurable marker ticking - nonEntityTicking.merge(key, 1, Integer::sum); - } + int count = info.stream().mapToInt(Pair::getRight).sum(); + int nonTickingCount = nonEntityTicking.values().stream().mapToInt(Integer::intValue).sum(); + sender.sendPlainMessage("Total Ticking: " + (count - nonTickingCount) + ", Total Non-Ticking: " + nonTickingCount); + info.forEach(e -> { + int nonTicking = nonEntityTicking.getOrDefault(e.getKey(), 0); + sender.sendMessage(text(" " + (e.getValue() - nonTicking) + " (" + nonTicking + ") " + ": " + e.getKey()) + .hoverEvent(text("Click to inspect " + e.getKey(), GREEN)) + .clickEvent(ClickEvent.suggestCommand("/paper:paper entity list " + e.getKey()))); }); - if (names.size() == 1) { - Identifier name = names.iterator().next(); - Pair> info = list.get(name); - int nonTicking = nonEntityTicking.getOrDefault(name, 0); - if (info == null) { - sender.sendMessage(text("No entities found.", RED)); - return; - } - sender.sendMessage("Entity: " + name + " Total Ticking: " + (info.getLeft() - nonTicking) + ", Total Non-Ticking: " + nonTicking); - info.getRight().entrySet().stream() - .sorted((a, b) -> !a.getValue().equals(b.getValue()) ? b.getValue() - a.getValue() : a.getKey().toString().compareTo(b.getKey().toString())) - .limit(10).forEach(e -> { - final int x = (e.getKey().x << 4) + 8; - final int z = (e.getKey().z << 4) + 8; - final Component message = text(" " + e.getValue() + ": " + e.getKey().x + ", " + e.getKey().z + (chunkProviderServer.isPositionTicking(e.getKey().toLong()) ? " (Ticking)" : " (Non-Ticking)")) - .hoverEvent(HoverEvent.showText(text("Click to teleport to chunk", GREEN))) - .clickEvent(ClickEvent.clickEvent(ClickEvent.Action.RUN_COMMAND, "/minecraft:execute as @s in " + world.getWorld().getKey() + " run tp " + x + " " + (world.getWorld().getHighestBlockYAt(x, z, HeightMap.MOTION_BLOCKING) + 1) + " " + z)); - sender.sendMessage(message); - }); - } else { - List> info = list.entrySet().stream() - .filter(e -> names.contains(e.getKey())) - .map(e -> Pair.of(e.getKey(), e.getValue().left)) - .sorted((a, b) -> !a.getRight().equals(b.getRight()) ? b.getRight() - a.getRight() : a.getKey().toString().compareTo(b.getKey().toString())) - .toList(); - - if (info.isEmpty()) { - sender.sendMessage(text("No entities found.", RED)); - return; - } - - int count = info.stream().mapToInt(Pair::getRight).sum(); - int nonTickingCount = nonEntityTicking.values().stream().mapToInt(Integer::intValue).sum(); - sender.sendMessage("Total Ticking: " + (count - nonTickingCount) + ", Total Non-Ticking: " + nonTickingCount); - info.forEach(e -> { - int nonTicking = nonEntityTicking.getOrDefault(e.getKey(), 0); - sender.sendMessage(" " + (e.getValue() - nonTicking) + " (" + nonTicking + ") " + ": " + e.getKey()); - }); - sender.sendMessage("* First number is ticking entities, second number is non-ticking entities"); - } + sender.sendPlainMessage("* First number is ticking entities, second number is non-ticking entities"); + return info.size(); } } } diff --git a/paper-server/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java b/paper-server/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java index cd2e4d792e97..bb4a4204e86e 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/subcommands/HeapDumpCommand.java @@ -1,38 +1,47 @@ package io.papermc.paper.command.subcommands; -import io.papermc.paper.command.PaperSubcommand; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import io.papermc.paper.command.PaperCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import java.nio.file.Path; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.craftbukkit.CraftServer; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.format.NamedTextColor.GREEN; import static net.kyori.adventure.text.format.NamedTextColor.RED; import static net.kyori.adventure.text.format.NamedTextColor.YELLOW; -@DefaultQualifier(NonNull.class) -public final class HeapDumpCommand implements PaperSubcommand { - @Override - public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { - this.dumpHeap(sender); - return true; +public final class HeapDumpCommand { + + public static LiteralArgumentBuilder create() { + return Commands.literal("heap") + .requires(PaperCommand.hasPermission("heap")) + .executes(context -> { + return dumpHeap(context.getSource().getSender()); + }); } - private void dumpHeap(final CommandSender sender) { - java.nio.file.Path dir = java.nio.file.Paths.get("./dumps"); - String name = "heap-dump-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()); + private static int dumpHeap(final CommandSender sender) { + Path dir = Path.of("dumps"); + String name = "heap-dump-" + PaperCommand.FILENAME_DATE_TIME_FORMATTER.format(LocalDateTime.now()); - Command.broadcastCommandMessage(sender, text("Writing JVM heap data...", YELLOW)); + sender.sendMessage(text("Writing JVM heap data...", YELLOW)); - java.nio.file.Path file = CraftServer.dumpHeap(dir, name); - if (file != null) { - Command.broadcastCommandMessage(sender, text("Heap dump saved to " + file, GREEN)); + Path path = CraftServer.dumpHeap(dir, name); + if (path != null) { + sender.sendMessage( + text("Heap dump saved to", GREEN) + .appendSpace() + .append(PaperCommand.asFriendlyPath(path)) + ); + return Command.SINGLE_SUCCESS; } else { - Command.broadcastCommandMessage(sender, text("Failed to write heap dump, see server log for details", RED)); + sender.sendMessage(text("Failed to write heap dump, see server log for details", RED)); + return 0; } } } diff --git a/paper-server/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java b/paper-server/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java index 950e709a7b4b..13221bcbb96f 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/subcommands/MobcapsCommand.java @@ -1,10 +1,14 @@ package io.papermc.paper.command.subcommands; -import com.google.common.collect.ImmutableMap; -import io.papermc.paper.command.CommandUtil; -import io.papermc.paper.command.PaperSubcommand; -import java.util.ArrayList; -import java.util.Collections; +import com.google.common.collect.Maps; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import io.papermc.paper.command.PaperCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.function.ToIntFunction; @@ -17,6 +21,7 @@ import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Util; import net.minecraft.world.entity.MobCategory; import net.minecraft.world.level.NaturalSpawner; import org.bukkit.Bukkit; @@ -25,155 +30,96 @@ import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.entity.CraftPlayer; import org.bukkit.entity.Player; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.framework.qual.DefaultQualifier; - -@DefaultQualifier(NonNull.class) -public final class MobcapsCommand implements PaperSubcommand { - static final Map MOB_CATEGORY_COLORS = ImmutableMap.builder() - .put(MobCategory.MONSTER, NamedTextColor.RED) - .put(MobCategory.CREATURE, NamedTextColor.GREEN) - .put(MobCategory.AMBIENT, NamedTextColor.GRAY) - .put(MobCategory.AXOLOTLS, TextColor.color(0x7324FF)) - .put(MobCategory.UNDERGROUND_WATER_CREATURE, TextColor.color(0x3541E6)) - .put(MobCategory.WATER_CREATURE, TextColor.color(0x006EFF)) - .put(MobCategory.WATER_AMBIENT, TextColor.color(0x00B3FF)) - .put(MobCategory.MISC, TextColor.color(0x636363)) - .build(); - - @Override - public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { - switch (subCommand) { - case "mobcaps" -> this.printMobcaps(sender, args); - case "playermobcaps" -> this.printPlayerMobcaps(sender, args); - } - return true; - } - @Override - public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { - return switch (subCommand) { - case "mobcaps" -> CommandUtil.getListMatchingLast(sender, args, this.suggestMobcaps(args)); - case "playermobcaps" -> CommandUtil.getListMatchingLast(sender, args, this.suggestPlayerMobcaps(sender, args)); - default -> throw new IllegalArgumentException(); - }; - } - - private List suggestMobcaps(final String[] args) { - if (args.length == 1) { - final List worlds = new ArrayList<>(Bukkit.getWorlds().stream().map(World::getName).toList()); - worlds.add("*"); - return worlds; - } - - return Collections.emptyList(); +import static net.kyori.adventure.text.Component.text; + +public final class MobcapsCommand { + + static final Map MOB_CATEGORY_COLORS = Maps.immutableEnumMap(Util.make(new EnumMap<>(MobCategory.class), map -> { + map.put(MobCategory.MONSTER, NamedTextColor.RED); + map.put(MobCategory.CREATURE, NamedTextColor.GREEN); + map.put(MobCategory.AMBIENT, NamedTextColor.GRAY); + map.put(MobCategory.AXOLOTLS, TextColor.color(0x7324FF)); + map.put(MobCategory.UNDERGROUND_WATER_CREATURE, TextColor.color(0x3541E6)); + map.put(MobCategory.WATER_CREATURE, TextColor.color(0x006EFF)); + map.put(MobCategory.WATER_AMBIENT, TextColor.color(0x00B3FF)); + map.put(MobCategory.MISC, TextColor.color(0x636363)); + })); + + public static LiteralArgumentBuilder createGlobal() { + return Commands.literal("mobcaps") + .requires(PaperCommand.hasPermission("mobcaps")) + .executes(context -> { + return printMobcaps(context.getSource().getSender(), List.of(context.getSource().getLocation().getWorld())); + }) + .then(Commands.literal("*") + .executes(context -> { + return printMobcaps(context.getSource().getSender(), Bukkit.getWorlds()); + }) + ) + .then(Commands.argument("world", ArgumentTypes.world()) + .executes(context -> { + return printMobcaps(context.getSource().getSender(), List.of(context.getArgument("world", World.class))); + }) + ); } - private List suggestPlayerMobcaps(final CommandSender sender, final String[] args) { - if (args.length == 1) { - final List list = new ArrayList<>(); - for (final Player player : Bukkit.getOnlinePlayers()) { - if (!(sender instanceof Player senderPlayer) || senderPlayer.canSee(player)) { - list.add(player.getName()); + public static LiteralArgumentBuilder createPlayer() { + return Commands.literal("playermobcaps") + .requires(PaperCommand.hasPermission("playermobcaps")) + .executes(context -> { + if (!(context.getSource().getExecutor() instanceof Player player)) { + throw net.minecraft.commands.CommandSourceStack.ERROR_NOT_PLAYER.create(); } - } - return list; - } - - return Collections.emptyList(); + return printPlayerMobcaps(context.getSource().getSender(), player); + }) + .then(Commands.argument("player", ArgumentTypes.player()) + .executes(context -> { + return printPlayerMobcaps( + context.getSource().getSender(), + context.getArgument("player", PlayerSelectorArgumentResolver.class).resolve(context.getSource()).getFirst() + ); + }) + ); } - private void printMobcaps(final CommandSender sender, final String[] args) { - final List worlds; - if (args.length == 0) { - if (sender instanceof Player player) { - worlds = List.of(player.getWorld()); - } else { - sender.sendMessage(Component.text("Must specify a world! ex: '/paper mobcaps world'", NamedTextColor.RED)); - return; - } - } else if (args.length == 1) { - final String input = args[0]; - if (input.equals("*")) { - worlds = Bukkit.getWorlds(); - } else { - final @Nullable World world = Bukkit.getWorld(input); - if (world == null) { - sender.sendMessage(Component.text("'" + input + "' is not a valid world!", NamedTextColor.RED)); - return; - } else { - worlds = List.of(world); - } - } - } else { - sender.sendMessage(Component.text("Too many arguments!", NamedTextColor.RED)); - return; - } - + private static int printMobcaps(final CommandSender sender, final List worlds) { for (final World world : worlds) { final ServerLevel level = ((CraftWorld) world).getHandle(); - final NaturalSpawner.@Nullable SpawnState state = level.getChunkSource().getLastSpawnState(); - - final int chunks; - if (state == null) { - chunks = 0; - } else { - chunks = state.getSpawnableChunkCount(); - } + final NaturalSpawner.SpawnState state = level.getChunkSource().getLastSpawnState(); + + final int chunks = state == null ? 0 : state.getSpawnableChunkCount(); sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), - Component.text("Mobcaps for world: "), - Component.text(world.getName(), NamedTextColor.AQUA), - Component.text(" (" + chunks + " spawnable chunks)") + text("Mobcaps for world: "), + text(world.key().asString(), NamedTextColor.AQUA), + text(" (" + chunks + " spawnable chunks)") )); sender.sendMessage(createMobcapsComponent( category -> { - if (state == null) { - return 0; - } else { - return state.getMobCategoryCounts().getOrDefault(category, 0); - } + return state == null ? 0 : state.getMobCategoryCounts().getOrDefault(category, 0); }, category -> NaturalSpawner.globalLimitForCategory(level, category, chunks) )); } + return worlds.size(); } - private void printPlayerMobcaps(final CommandSender sender, final String[] args) { - final @Nullable Player player; - if (args.length == 0) { - if (sender instanceof Player pl) { - player = pl; - } else { - sender.sendMessage(Component.text("Must specify a player! ex: '/paper playermobcount playerName'", NamedTextColor.RED)); - return; - } - } else if (args.length == 1) { - final String input = args[0]; - player = Bukkit.getPlayerExact(input); - if (player == null) { - sender.sendMessage(Component.text("Could not find player named '" + input + "'", NamedTextColor.RED)); - return; - } - } else { - sender.sendMessage(Component.text("Too many arguments!", NamedTextColor.RED)); - return; - } - + private static int printPlayerMobcaps(final CommandSender sender, final Player player) { final ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle(); final ServerLevel level = serverPlayer.level(); if (!level.paperConfig().entities.spawning.perPlayerMobSpawns) { - sender.sendMessage(Component.text("Use '/paper mobcaps' for worlds where per-player mob spawning is disabled.", NamedTextColor.RED)); - return; + sender.sendMessage(text("Use '/paper mobcaps' for worlds where per-player mob spawning is disabled.", NamedTextColor.RED)); + return 0; } - sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), Component.text("Mobcaps for player: "), Component.text(player.getName(), NamedTextColor.GREEN))); + sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), text("Mobcaps for player: "), text(player.getName(), NamedTextColor.GREEN))); sender.sendMessage(createMobcapsComponent( category -> level.chunkSource.chunkMap.getMobCountNear(serverPlayer, category), category -> level.getWorld().getSpawnLimitUnsafe(org.bukkit.craftbukkit.util.CraftSpawnCategory.toBukkit(category)) )); + return Command.SINGLE_SUCCESS; } private static Component createMobcapsComponent(final ToIntFunction countGetter, final ToIntFunction limitGetter) { @@ -183,43 +129,43 @@ private static Component createMobcapsComponent(final ToIntFunction final TextColor color = entry.getValue(); final Component categoryHover = Component.join(JoinConfiguration.noSeparators(), - Component.text("Entity types in category ", TextColor.color(0xE0E0E0)), - Component.text(category.getName(), color), - Component.text(':', NamedTextColor.GRAY), + text("Entity types in category ", TextColor.color(0xE0E0E0)), + text(category.getName(), color), + text(':', NamedTextColor.GRAY), Component.newline(), Component.newline(), BuiltInRegistries.ENTITY_TYPE.entrySet().stream() .filter(it -> it.getValue().getCategory() == category) .map(it -> Component.translatable(it.getValue().getDescriptionId())) - .collect(Component.toComponent(Component.text(", ", NamedTextColor.GRAY))) + .collect(Component.toComponent(text(", ", NamedTextColor.GRAY))) ); - final Component categoryComponent = Component.text() + final Component categoryComponent = text() .content(" " + category.getName()) .color(color) .hoverEvent(categoryHover) .build(); - final TextComponent.Builder builder = Component.text() + final TextComponent.Builder builder = text() .append( categoryComponent, - Component.text(": ", NamedTextColor.GRAY) + text(": ", NamedTextColor.GRAY) ); final int limit = limitGetter.applyAsInt(category); if (limit != -1) { builder.append( - Component.text(countGetter.applyAsInt(category)), - Component.text("/", NamedTextColor.GRAY), - Component.text(limit) + text(countGetter.applyAsInt(category)), + text("/", NamedTextColor.GRAY), + text(limit) ); } else { - builder.append(Component.text() + builder.append(text() .append( - Component.text('n'), - Component.text("/", NamedTextColor.GRAY), - Component.text('a') + text('n'), + text("/", NamedTextColor.GRAY), + text('a') ) - .hoverEvent(Component.text("This category does not naturally spawn."))); + .hoverEvent(text("This category does not naturally spawn."))); } return builder; }) diff --git a/paper-server/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java b/paper-server/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java index bd68139ae635..1ae07ffb60b2 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/subcommands/ReloadCommand.java @@ -1,33 +1,37 @@ package io.papermc.paper.command.subcommands; -import io.papermc.paper.command.PaperSubcommand; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import io.papermc.paper.command.PaperCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; import net.minecraft.server.MinecraftServer; -import org.bukkit.command.Command; import org.bukkit.command.CommandSender; -import org.bukkit.craftbukkit.CraftServer; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.format.NamedTextColor.GREEN; import static net.kyori.adventure.text.format.NamedTextColor.RED; +import static org.bukkit.command.Command.broadcastCommandMessage; -@DefaultQualifier(NonNull.class) -public final class ReloadCommand implements PaperSubcommand { - @Override - public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { - this.doReload(sender); - return true; +public final class ReloadCommand { + + public static LiteralArgumentBuilder create() { + return Commands.literal("reload") + .requires(PaperCommand.hasPermission("reload")) + .executes(context -> { + doReload(context.getSource().getSender()); + return Command.SINGLE_SUCCESS; + }); } - private void doReload(final CommandSender sender) { - Command.broadcastCommandMessage(sender, text("Please note that this command is not supported and may cause issues.", RED)); - Command.broadcastCommandMessage(sender, text("If you encounter any issues please use the /stop command to restart your server.", RED)); + private static void doReload(final CommandSender sender) { + broadcastCommandMessage(sender, text("Please note that this command is not supported and may cause issues.", RED)); + broadcastCommandMessage(sender, text("If you encounter any issues please use the /stop command to restart your server.", RED)); - MinecraftServer server = ((CraftServer) sender.getServer()).getServer(); + MinecraftServer server = MinecraftServer.getServer(); server.paperConfigurations.reloadConfigs(server); server.server.reloadCount++; - Command.broadcastCommandMessage(sender, text("Paper config reload complete.", GREEN)); + broadcastCommandMessage(sender, text("Paper config reload complete.", GREEN)); } } diff --git a/paper-server/src/main/java/io/papermc/paper/command/subcommands/SyncLoadInfoCommand.java b/paper-server/src/main/java/io/papermc/paper/command/subcommands/SyncLoadInfoCommand.java index 95d6022c9cfb..0ca3e5fc2aeb 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/subcommands/SyncLoadInfoCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/subcommands/SyncLoadInfoCommand.java @@ -2,87 +2,75 @@ import com.destroystokyo.paper.io.SyncLoadFinder; import com.google.gson.JsonObject; +import com.google.gson.Strictness; import com.google.gson.internal.Streams; import com.google.gson.stream.JsonWriter; -import io.papermc.paper.command.CommandUtil; -import io.papermc.paper.command.PaperSubcommand; -import java.io.File; -import java.io.FileOutputStream; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import io.papermc.paper.command.PaperCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; import java.io.PrintStream; import java.io.StringWriter; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; - -import net.kyori.adventure.text.event.ClickEvent; -import net.kyori.adventure.text.event.HoverEvent; import net.minecraft.server.MinecraftServer; import org.bukkit.command.CommandSender; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; import static net.kyori.adventure.text.Component.text; -import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GOLD; import static net.kyori.adventure.text.format.NamedTextColor.GREEN; import static net.kyori.adventure.text.format.NamedTextColor.RED; -import static net.kyori.adventure.text.format.NamedTextColor.WHITE; -@DefaultQualifier(NonNull.class) -public final class SyncLoadInfoCommand implements PaperSubcommand { - @Override - public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { - this.doSyncLoadInfo(sender, args); - return true; - } +public final class SyncLoadInfoCommand { - @Override - public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { - return CommandUtil.getListMatchingLast(sender, args, "clear"); + public static LiteralArgumentBuilder create() { + return Commands.literal("syncloadinfo") + .requires(PaperCommand.hasPermission("syncloadinfo").and($ -> SyncLoadFinder.ENABLED)) + .executes(context -> { + return dumpSyncLoadInfo(context.getSource().getSender()); + }) + .then(Commands.literal("clear") + .executes(context -> { + SyncLoadFinder.clear(); + context.getSource().getSender().sendMessage(text("Sync load data cleared.", GOLD)); + return Command.SINGLE_SUCCESS; + }) + ); } - private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"); - - private void doSyncLoadInfo(final CommandSender sender, final String[] args) { - if (!SyncLoadFinder.ENABLED) { - String systemFlag = "-Dpaper.debug-sync-loads=true"; - sender.sendMessage(text().color(RED).append(text("This command requires the server startup flag '")).append( - text(systemFlag, WHITE).clickEvent(ClickEvent.copyToClipboard(systemFlag)) - .hoverEvent(HoverEvent.showText(text("Click to copy the system flag")))).append( - text("' to be set."))); - return; - } - - if (args.length > 0 && args[0].equals("clear")) { - SyncLoadFinder.clear(); - sender.sendMessage(text("Sync load data cleared.", GRAY)); - return; - } - - File file = new File(new File(new File("."), "debug"), - "sync-load-info-" + FORMATTER.format(LocalDateTime.now()) + ".txt"); - file.getParentFile().mkdirs(); - sender.sendMessage(text("Writing sync load info to " + file, GREEN)); - + private static int dumpSyncLoadInfo(final CommandSender sender) { + final Path path = Path.of("debug", "sync-load-info-" + PaperCommand.FILENAME_DATE_TIME_FORMATTER.format(LocalDateTime.now()) + ".txt"); + sender.sendMessage( + text("Writing sync load info into", GREEN) + .appendSpace() + .append(PaperCommand.asFriendlyPath(path)) + ); try { + Files.createDirectories(path.getParent()); + final JsonObject data = SyncLoadFinder.serialize(); - StringWriter stringWriter = new StringWriter(); + final StringWriter stringWriter = new StringWriter(); JsonWriter jsonWriter = new JsonWriter(stringWriter); jsonWriter.setIndent(" "); - jsonWriter.setLenient(false); + jsonWriter.setStrictness(Strictness.STRICT); Streams.write(data, jsonWriter); try ( - PrintStream out = new PrintStream(new FileOutputStream(file), false, StandardCharsets.UTF_8) + final PrintStream out = new PrintStream(Files.newOutputStream(path), false, StandardCharsets.UTF_8) ) { out.print(stringWriter); } sender.sendMessage(text("Successfully written sync load information!", GREEN)); + return Command.SINGLE_SUCCESS; } catch (Throwable thr) { sender.sendMessage(text("Failed to write sync load information! See the console for more info.", RED)); MinecraftServer.LOGGER.warn("Error occurred while dumping sync chunk load info", thr); + return 0; } } } diff --git a/paper-server/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java b/paper-server/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java index 58ca24715eaf..e1104c1330b3 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java +++ b/paper-server/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java @@ -1,21 +1,16 @@ package io.papermc.paper.command.subcommands; -import io.papermc.paper.command.PaperSubcommand; -import net.minecraft.server.MinecraftServer; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.framework.qual.DefaultQualifier; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import io.papermc.paper.command.PaperCommand; +import io.papermc.paper.command.PaperVersionCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; -@DefaultQualifier(NonNull.class) -public final class VersionCommand implements PaperSubcommand { - @Override - public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { - final @Nullable Command redirect = MinecraftServer.getServer().server.getCommandMap().getCommand("version"); - if (redirect != null) { - redirect.execute(sender, "paper", new String[0]); - } - return true; +public final class VersionCommand { + + public static LiteralArgumentBuilder create(String name) { + return Commands.literal(name) + .requires(PaperCommand.hasPermission("version")) + .executes(PaperVersionCommand::serverVersion); } } diff --git a/paper-server/src/main/java/io/papermc/paper/command/subcommands/package-info.java b/paper-server/src/main/java/io/papermc/paper/command/subcommands/package-info.java new file mode 100644 index 000000000000..f8198f5caacd --- /dev/null +++ b/paper-server/src/main/java/io/papermc/paper/command/subcommands/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package io.papermc.paper.command.subcommands; + +import org.jspecify.annotations.NullMarked; diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java index b2961752b50e..a26c66b3f25a 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -12,6 +12,7 @@ import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.serialization.Dynamic; import com.mojang.serialization.Lifecycle; +import io.papermc.paper.command.PaperCommand; import io.papermc.paper.configuration.GlobalConfiguration; import io.papermc.paper.configuration.PaperServerConfiguration; import io.papermc.paper.configuration.ServerConfiguration; @@ -416,7 +417,11 @@ public CraftPlayer apply(ServerPlayer player) { this.pluginManager = new SimplePluginManager(this, commandMap); this.paperPluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(this, this.commandMap, pluginManager); this.pluginManager.paperPluginManager = this.paperPluginManager; - // Paper end + // Paper end + + // We have to register the permissions of the PaperCommand **after** the Commands have already been registered + // because of the pluginManager not being set then. + PaperCommand.registerPermissions(); CraftRegistry.setMinecraftRegistry(console.registryAccess()); @@ -1022,7 +1027,7 @@ public void reload() { // Paper end this.reloadData(); org.spigotmc.SpigotConfig.registerCommands(); // Spigot - io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper + io.papermc.paper.command.PaperCommands.registerLegacyCommands(this.console); // Paper this.spark.registerCommandBeforePlugins(this); // Paper - spark this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); diff --git a/paper-server/src/main/java/org/spigotmc/TicksPerSecondCommand.java b/paper-server/src/main/java/org/spigotmc/TicksPerSecondCommand.java index 2756ca738b99..5eb0a3e02bcf 100644 --- a/paper-server/src/main/java/org/spigotmc/TicksPerSecondCommand.java +++ b/paper-server/src/main/java/org/spigotmc/TicksPerSecondCommand.java @@ -21,7 +21,7 @@ public class TicksPerSecondCommand extends Command { public TicksPerSecondCommand(String name) { super(name); this.description = "Gets the current ticks per second for the server"; - this.usageMessage = "/tps"; + this.usageMessage = "/tps [mem]"; this.setPermission("bukkit.command.tps"); }