From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Mon, 29 Feb 2016 21:02:09 -0600 Subject: [PATCH] Paper command diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..0c7738412ce5c9b8c66e3f1c1e13fcd2a2154195 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java @@ -0,0 +1,289 @@ +package com.destroystokyo.paper; + +import com.google.common.base.Functions; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +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.Location; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.entity.Player; + +import java.io.File; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +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; + +public class PaperCommand extends Command { + private static final String BASE_PERM = "bukkit.command.paper."; + private static final ImmutableSet SUBCOMMANDS = ImmutableSet.builder().add("heap", "entity", "reload", "version").build(); + + public PaperCommand(String name) { + super(name); + this.description = "Paper related commands"; + this.usageMessage = "/paper [" + Joiner.on(" | ").join(SUBCOMMANDS) + "]"; + this.setPermission("bukkit.command.paper;" + Joiner.on(';').join(SUBCOMMANDS.stream().map(s -> BASE_PERM + s).collect(Collectors.toSet()))); + } + + private static boolean testPermission(CommandSender commandSender, String permission) { + if (commandSender.hasPermission(BASE_PERM + permission) || commandSender.hasPermission("bukkit.command.paper")) return true; + commandSender.sendMessage(text("I'm sorry, but you do not have permission to perform this command. Please contact the server administrators if you believe that this is in error.", RED)); + return false; + } + + @Override + public List tabComplete(CommandSender sender, String alias, String[] args, Location location) throws IllegalArgumentException { + if (args.length <= 1) + return getListMatchingLast(sender, args, SUBCOMMANDS); + + switch (args[0].toLowerCase(Locale.ENGLISH)) + { + case "entity": + if (args.length == 2) + return getListMatchingLast(sender, args, "help", "list"); + if (args.length == 3) + return getListMatchingLast(sender, args, EntityType.getEntityNameList().stream().map(ResourceLocation::toString).sorted().toArray(String[]::new)); + break; + } + return Collections.emptyList(); + } + + // Code from Mojang - copyright them + public static List getListMatchingLast(CommandSender sender, String[] args, String... matches) { + return getListMatchingLast(sender, args, (Collection) Arrays.asList(matches)); + } + + public static boolean matches(String s, String s1) { + return s1.regionMatches(true, 0, s, 0, s.length()); + } + + public static List getListMatchingLast(CommandSender sender, String[] strings, 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(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 ResourceLocation && matches(last, ((ResourceLocation) object).getPath())) { + results.add(String.valueOf(object)); + } + } + } + } + + return results; + } + // end copy stuff + + @Override + public boolean execute(CommandSender sender, String commandLabel, String[] args) { + if (!testPermission(sender)) return true; + + if (args.length == 0) { + sender.sendMessage(text("Usage: " + this.usageMessage, RED)); + return false; + } + if (SUBCOMMANDS.contains(args[0].toLowerCase(Locale.ENGLISH))) { + if (!testPermission(sender, args[0].toLowerCase(Locale.ENGLISH))) return true; + } + switch (args[0].toLowerCase(Locale.ENGLISH)) { + case "heap": + dumpHeap(sender); + break; + case "entity": + listEntities(sender, args); + break; + case "reload": + doReload(sender); + break; + case "ver": + if (!testPermission(sender, "version")) break; // "ver" needs a special check because it's an alias. All other commands are checked up before the switch statement (because they are present in the SUBCOMMANDS set) + case "version": + Command ver = MinecraftServer.getServer().server.getCommandMap().getCommand("version"); + if (ver != null) { + ver.execute(sender, commandLabel, new String[0]); + break; + } + // else - fall through to default + default: + sender.sendMessage(text("Usage: " + this.usageMessage, RED)); + return false; + } + + return true; + } + + /* + * Ported from MinecraftForge - author: LexManos - License: LGPLv2.1 + */ + private void listEntities(CommandSender sender, String[] args) { + if (args.length < 2 || args[1].toLowerCase(Locale.ENGLISH).equals("help")) { + sender.sendMessage(text("Use /paper entity [list] help for more information on a specific command", RED)); + return; + } + + switch (args[1].toLowerCase(Locale.ENGLISH)) { + case "list": + String filter = "*"; + if (args.length > 2) { + if (args[2].toLowerCase(Locale.ENGLISH).equals("help")) { + sender.sendMessage(text("Use /paper entity list [filter] [worldName] to get entity info that matches the optional filter.", RED)); + return; + } + filter = args[2]; + } + final String cleanfilter = filter.replace("?", ".?").replace("*", ".*?"); + Set names = EntityType.getEntityNameList().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; + } + + String worldName; + if (args.length > 3) { + worldName = args[3]; + } 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(); + 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; + } + ServerLevel world = ((CraftWorld) Bukkit.getWorld(worldName)).getHandle(); + + Map nonEntityTicking = Maps.newHashMap(); + ServerChunkCache chunkProviderServer = world.getChunkSource(); + + world.getAllEntities().forEach(e -> { + ResourceLocation 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 (!chunkProviderServer.isPositionTicking(e)) { + nonEntityTicking.merge(key, Integer.valueOf(1), Integer::sum); + } + }); + + if (names.size() == 1) { + ResourceLocation name = names.iterator().next(); + Pair> info = list.get(name); + int nonTicking = nonEntityTicking.getOrDefault(name, Integer.valueOf(0)).intValue(); + 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 -> sender.sendMessage(" " + e.getValue() + ": " + e.getKey().x + ", " + e.getKey().z + (chunkProviderServer.isPositionTicking(e.getKey().toLong()) ? " (Ticking)" : " (Non-Ticking)"))); + } 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())) + .collect(Collectors.toList()); + + if (info == null || info.size() == 0) { + 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(), Integer.valueOf(0)).intValue(); + sender.sendMessage(" " + (e.getValue() - nonTicking) + " (" + nonTicking + ") " + ": " + e.getKey()); + }); + sender.sendMessage("* First number is ticking entities, second number is non-ticking entities"); + } + break; + } + } + + private void dumpHeap(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()); + + Command.broadcastCommandMessage(sender, 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)); + } else { + Command.broadcastCommandMessage(sender, text("Failed to write heap dump, see server log for details", RED)); + } + } + + private void doReload(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)); + + MinecraftServer console = MinecraftServer.getServer(); + com.destroystokyo.paper.PaperConfig.init((File) console.options.valueOf("paper-settings")); + for (ServerLevel world : console.getAllLevels()) { + world.paperConfig.init(); + } + console.server.reloadCount++; + + Command.broadcastCommandMessage(sender, text("Paper config reload complete.", GREEN)); + } +}