From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Warrior <50800980+Warriorrrr@users.noreply.github.com> Date: Tue, 25 Oct 2022 21:15:37 +0200 Subject: [PATCH] Add /paper dumplisteners command Co-authored-by: TwoLeggedCat <80929284+TwoLeggedCat@users.noreply.github.com> diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java index c9bb2df0d884227576ed8d2e72219bbbd7ba827e..534d9c380f26d6cce3c99fa88ad2e15410535094 100644 --- a/src/main/java/io/papermc/paper/command/PaperCommand.java +++ b/src/main/java/io/papermc/paper/command/PaperCommand.java @@ -41,6 +41,7 @@ public final class PaperCommand extends Command { 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()); return commands.entrySet().stream() .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) diff --git a/src/main/java/io/papermc/paper/command/subcommands/DumpItemCommand.java b/src/main/java/io/papermc/paper/command/subcommands/DumpItemCommand.java index efc4ec58ab4b3ceefd66b714d286a6187babc724..fae73bac86fea722be4c9178eda0779ea88d1884 100644 --- a/src/main/java/io/papermc/paper/command/subcommands/DumpItemCommand.java +++ b/src/main/java/io/papermc/paper/command/subcommands/DumpItemCommand.java @@ -1,19 +1,26 @@ package io.papermc.paper.command.subcommands; import io.papermc.paper.adventure.PaperAdventure; +import io.papermc.paper.command.CommandUtil; import io.papermc.paper.command.PaperSubcommand; import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; import net.kyori.adventure.text.Component; 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.nbt.NbtOps; import net.minecraft.nbt.NbtUtils; @@ -25,6 +32,7 @@ import org.bukkit.craftbukkit.CraftServer; 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 net.kyori.adventure.text.Component.join; @@ -36,17 +44,19 @@ import static net.kyori.adventure.text.format.NamedTextColor.GRAY; import static net.kyori.adventure.text.format.NamedTextColor.RED; import static net.kyori.adventure.text.format.NamedTextColor.WHITE; import static net.kyori.adventure.text.format.NamedTextColor.YELLOW; +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); + this.doDumpItem(sender, args.length > 0 && "all".equals(args[0])); return true; } - private void doDumpItem(final CommandSender sender) { + @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; @@ -57,38 +67,65 @@ public final class DumpItemCommand implements PaperSubcommand { final String itemName = itemStack.getItemHolder().unwrapKey().orElseThrow().location().toString(); itemCommandBuilder.append(itemName); visualOutput.append(text(itemName, YELLOW)); // item type + final Set> referencedComponentTypes = Collections.newSetFromMap(new IdentityHashMap<>()); final DataComponentPatch patch = itemStack.getComponentsPatch(); + referencedComponentTypes.addAll(patch.entrySet().stream().map(Map.Entry::getKey).toList()); + final DataComponentMap prototype = itemStack.getItem().components(); + if (includeAllComponents) { + referencedComponentTypes.addAll(prototype.keySet()); + } final RegistryAccess.Frozen access = ((CraftServer) sender.getServer()).getServer().registryAccess(); final RegistryOps ops = access.createSerializationContext(NbtOps.INSTANCE); final Registry> registry = access.registryOrThrow(Registries.DATA_COMPONENT_TYPE); - if (!patch.isEmpty()) { - visualOutput.append(text("[", WHITE)); - itemCommandBuilder.append("["); - final List componentComponents = new ArrayList<>(); - final List commandComponents = new ArrayList<>(); - for (final Map.Entry, Optional> entry : patch.entrySet()) { - final String path = registry.getResourceKey(entry.getKey()).orElseThrow().location().getPath(); - if (entry.getValue().isEmpty()) { + final List componentComponents = new ArrayList<>(); + final List commandComponents = new ArrayList<>(); + for (final DataComponentType type : referencedComponentTypes) { + final String path = registry.getResourceKey(type).orElseThrow().location().getPath(); + final @Nullable Optional patchedValue = patch.get(type); + final @Nullable TypedDataComponent prototypeValue = prototype.getTyped(type); + if (patchedValue != null) { + if (patchedValue.isEmpty()) { componentComponents.add(text().append(text('!', RED), text(path, AQUA))); commandComponents.add("!" + path); } else { - final Tag serialized = (Tag) ((DataComponentType) entry.getKey()).codecOrThrow().encodeStart(ops, entry.getValue().get()).getOrThrow(); - componentComponents.add(textOfChildren( - text(path, AQUA), - text("=", WHITE), - PaperAdventure.asAdventure(NbtUtils.toPrettyComponent(serialized)) - )); - commandComponents.add(path + "=" + serialized.getAsString()); + final Tag serialized = (Tag) ((DataComponentType) type).codecOrThrow().encodeStart(ops, patchedValue.get()).getOrThrow(); + writeComponentValue(componentComponents::add, commandComponents::add, path, serialized); } - + } else if (includeAllComponents && prototypeValue != null) { + final Tag serialized = prototypeValue.encodeValue(ops).getOrThrow(); + writeComponentValue(componentComponents::add, commandComponents::add, path, serialized); } - visualOutput - .append(join(JoinConfiguration.separator(text(",", WHITE)), componentComponents)) - .append(text("]", WHITE)); - itemCommandBuilder.append(String.join(",", commandComponents)).append("]"); + } + if (!componentComponents.isEmpty()) { + visualOutput.append( + text("[", color(0x8910CE)), + join(JoinConfiguration.separator(text(",", GRAY)), componentComponents), + text("]", color(0x8910CE)) + ); + itemCommandBuilder + .append("[") + .append(String.join(",", commandComponents)) + .append("]"); } final Component hoverMsg = text("Click to copy item definition to clipboard for use with /pgive", GRAY, ITALIC); player.sendMessage(visualOutput.build().compact().hoverEvent(hoverMsg).clickEvent(copyToClipboard(itemCommandBuilder.toString()))); } + + private static void writeComponentValue(final Consumer visualOutput, final Consumer commandOutput, final String path, final Tag serialized) { + visualOutput.accept(textOfChildren( + text(path, color(0xFF7FD7)), + text("=", WHITE), + PaperAdventure.asAdventure(NbtUtils.toPrettyComponent(serialized)) + )); + commandOutput.accept(path + "=" + serialized.getAsString()); + } + + @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/src/main/java/io/papermc/paper/command/subcommands/DumpListenersCommand.java b/src/main/java/io/papermc/paper/command/subcommands/DumpListenersCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..aa44d4685de3caee4131449bead7a084868ff976 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/subcommands/DumpListenersCommand.java @@ -0,0 +1,172 @@ +package io.papermc.paper.command.subcommands; + +import com.destroystokyo.paper.util.SneakyThrow; +import io.papermc.paper.command.PaperSubcommand; +import java.io.File; +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.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 net.kyori.adventure.text.Component; +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.framework.qual.DefaultQualifier; + +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 MethodHandle EVENT_TYPES_HANDLE; + + static { + try { + final Field eventTypesField = HandlerList.class.getDeclaredField("EVENT_TYPES"); + eventTypesField.setAccessible(true); + EVENT_TYPES_HANDLE = MethodHandles.lookup().unreflectGetter(eventTypesField); + } 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("tofile")) { + this.dumpToFile(sender); + return true; + } + this.doDumpListeners(sender, args); + return true; + } + + private void dumpToFile(final CommandSender sender) { + final File file = new File("debug/listeners-" + + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()) + ".txt"); + file.getParentFile().mkdirs(); + try (final PrintWriter writer = new PrintWriter(file)) { + for (final String eventClass : eventClassNames()) { + final HandlerList handlers; + try { + handlers = (HandlerList) findClass(eventClass).getMethod("getHandlerList").invoke(null); + } catch (final ReflectiveOperationException e) { + continue; + } + if (handlers.getRegisteredListeners().length != 0) { + writer.println(eventClass); + } + for (final RegisteredListener registeredListener : handlers.getRegisteredListeners()) { + writer.println(" - " + registeredListener); + } + } + } catch (final IOException ex) { + throw new RuntimeException(ex); + } + sender.sendMessage(text("Dumped listeners to " + file, GREEN)); + } + + private void doDumpListeners(final CommandSender sender, final String[] args) { + if (args.length == 0) { + sender.sendMessage(text("Usage: /paper dumplisteners tofile|", RED)); + return; + } + + try { + final HandlerList handlers = (HandlerList) findClass(args[0]).getMethod("getHandlerList").invoke(null); + + if (handlers.getRegisteredListeners().length == 0) { + sender.sendMessage(text(args[0] + " does not have any registered listeners.")); + return; + } + + sender.sendMessage(text("Listeners for " + args[0] + ":")); + + for (final RegisteredListener listener : handlers.getRegisteredListeners()) { + final Component hoverText = text("Priority: " + listener.getPriority().name() + " (" + listener.getPriority().getSlot() + ")", WHITE) + .append(newline()) + .append(text("Listener: " + listener.getListener())) + .append(newline()) + .append(text("Executor: " + listener.getExecutor())) + .append(newline()) + .append(text("Ignoring cancelled: " + listener.isIgnoringCancelled())); + + sender.sendMessage(text(listener.getPlugin().getName(), GREEN) + .append(space()) + .append(text("(" + listener.getListener().getClass().getName() + ")", GRAY).hoverEvent(hoverText))); + } + + sender.sendMessage(text("Total listeners: " + handlers.getRegisteredListeners().length)); + + } catch (final ClassNotFoundException e) { + sender.sendMessage(text("Unable to find a class named '" + args[0] + "'. Make sure to use the fully qualified name.", RED)); + } catch (final NoSuchMethodException e) { + sender.sendMessage(text("Class '" + args[0] + "' does not have a valid getHandlerList method.", RED)); + } catch (final ReflectiveOperationException e) { + sender.sendMessage(text("Something went wrong, see the console for more details.", RED)); + MinecraftServer.LOGGER.warn("Error occurred while dumping listeners for class " + args[0], 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("tofile"); + ret.addAll(eventClassNames()); + return ret; + } + + @SuppressWarnings("unchecked") + private static Set eventClassNames() { + try { + return (Set) EVENT_TYPES_HANDLE.invokeExact(); + } catch (final Throwable e) { + SneakyThrow.sneaky(e); + return Collections.emptySet(); // Unreachable + } + } + + private static Class findClass(final String className) throws ClassNotFoundException { + try { + return Class.forName(className); + } catch (final ClassNotFoundException ignore) { + for (final Plugin plugin : Bukkit.getServer().getPluginManager().getPlugins()) { + if (!plugin.isEnabled()) { + continue; + } + + try { + return Class.forName(className, false, plugin.getClass().getClassLoader()); + } catch (final ClassNotFoundException ignore0) { + } + } + } + throw new ClassNotFoundException(className); + } +}